git-cli-scanner 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +122 -2
  2. package/package.json +1 -1
  3. package/readme.md +17 -1
package/dist/cli.js CHANGED
@@ -32,6 +32,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
33
 
34
34
  // src/utils/git.ts
35
+ var git_exports = {};
36
+ __export(git_exports, {
37
+ getHistoryDiffs: () => getHistoryDiffs,
38
+ getStagedDiff: () => getStagedDiff
39
+ });
35
40
  async function getStagedDiff() {
36
41
  try {
37
42
  const { stdout: filesOutput } = await execAsync("git diff --cached --name-only");
@@ -50,6 +55,64 @@ async function getStagedDiff() {
50
55
  return [];
51
56
  }
52
57
  }
58
+ async function getHistoryDiffs(options) {
59
+ try {
60
+ let cmd = 'git log -p --pretty=format:"---COMMIT:%H---"';
61
+ if (options.id) {
62
+ cmd += ` -1 ${options.id}`;
63
+ } else {
64
+ if (options.all) cmd += " --all";
65
+ if (options.since) cmd += ` --since="${options.since}"`;
66
+ if (!options.all && !options.since) {
67
+ cmd += " -1";
68
+ }
69
+ }
70
+ const { stdout } = await execAsync(cmd, { maxBuffer: 1024 * 1024 * 50 });
71
+ if (!stdout.trim()) {
72
+ return [];
73
+ }
74
+ const commits = [];
75
+ let currentCommit = null;
76
+ let currentFile = null;
77
+ let currentDiff = "";
78
+ const lines = stdout.split("\n");
79
+ for (const line of lines) {
80
+ if (line.startsWith("---COMMIT:")) {
81
+ if (currentFile && currentCommit) {
82
+ currentCommit.diffs.push({ file: currentFile, content: currentDiff });
83
+ }
84
+ currentFile = null;
85
+ currentDiff = "";
86
+ const match = line.match(/---COMMIT:([a-f0-9]+)---/);
87
+ if (match) {
88
+ currentCommit = { commitHash: match[1], diffs: [] };
89
+ commits.push(currentCommit);
90
+ }
91
+ } else if (line.startsWith("diff --git a/")) {
92
+ if (currentFile && currentCommit) {
93
+ currentCommit.diffs.push({ file: currentFile, content: currentDiff });
94
+ }
95
+ const parts = line.substring(13).split(" b/");
96
+ if (parts.length > 0) {
97
+ currentFile = parts[0];
98
+ }
99
+ currentDiff = "";
100
+ } else if (currentFile) {
101
+ currentDiff += line + "\n";
102
+ }
103
+ }
104
+ if (currentFile && currentCommit) {
105
+ currentCommit.diffs.push({ file: currentFile, content: currentDiff });
106
+ }
107
+ return commits;
108
+ } catch (error2) {
109
+ if (error2.message && error2.message.includes("unknown revision")) {
110
+ throw new Error(`Invalid commit hash: ${options.id}`);
111
+ }
112
+ console.warn("Could not retrieve git history diffs. Are you in a git repository?");
113
+ return [];
114
+ }
115
+ }
53
116
  var import_child_process, import_util, execAsync;
54
117
  var init_git = __esm({
55
118
  "src/utils/git.ts"() {
@@ -477,7 +540,8 @@ var init_dummy = __esm({
477
540
  var scanner_exports = {};
478
541
  __export(scanner_exports, {
479
542
  scanDiff: () => scanDiff,
480
- scanDirectory: () => scanDirectory
543
+ scanDirectory: () => scanDirectory,
544
+ scanHistoryDiffs: () => scanHistoryDiffs
481
545
  });
482
546
  async function scanDiff() {
483
547
  const diffs = await getStagedDiff();
@@ -509,6 +573,28 @@ async function scanDirectory(dirPath) {
509
573
  }
510
574
  return issues;
511
575
  }
576
+ async function scanHistoryDiffs(options) {
577
+ const { getHistoryDiffs: getHistoryDiffs2 } = await Promise.resolve().then(() => (init_git(), git_exports));
578
+ const commitDiffs = await getHistoryDiffs2(options);
579
+ const issues = [];
580
+ for (const commitDiff of commitDiffs) {
581
+ for (const diff of commitDiff.diffs) {
582
+ for (const scanner of allScanners) {
583
+ const scannerIssues = scanner.scan(diff);
584
+ for (const issue of scannerIssues) {
585
+ issue.commitHash = commitDiff.commitHash;
586
+ issues.push(issue);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ for (const issue of issues) {
592
+ if (isDummySecret(issue.match)) {
593
+ issue.severity = "dummy";
594
+ }
595
+ }
596
+ return issues;
597
+ }
512
598
  var init_scanner = __esm({
513
599
  "src/scanner/index.ts"() {
514
600
  "use strict";
@@ -573,6 +659,9 @@ function printIssues(issues, showSolution = false) {
573
659
  console.error(`
574
660
  ${colorFn(indicator)} ${colorFn(import_picocolors.default.bold(label))} ${import_picocolors.default.dim("\xB7")} ${issue.type}`);
575
661
  console.error(` ${import_picocolors.default.dim("File:")} ${issue.file}:${issue.line || "?"}`);
662
+ if (issue.commitHash) {
663
+ console.error(` ${import_picocolors.default.dim("Commit:")} ${import_picocolors.default.cyan(issue.commitHash)}`);
664
+ }
576
665
  console.error(` ${import_picocolors.default.dim("Match:")} ${displayMatch}`);
577
666
  if (issue.risk) {
578
667
  console.error(` ${import_picocolors.default.dim("Risk:")} ${import_picocolors.default.yellow(issue.risk)}`);
@@ -812,7 +901,7 @@ var import_child_process2 = require("child_process");
812
901
  var fs3 = __toESM(require("fs"));
813
902
  var path3 = __toESM(require("path"));
814
903
  var program = new import_commander.Command();
815
- program.name("git-cli-scanner").description("Interactive Git hooks vulnerability scanner").version("1.2.1");
904
+ program.name("git-cli-scanner").description("Interactive Git hooks vulnerability scanner").version("1.3.1");
816
905
  program.command("init").description("Install pre-commit hook for automatic scanning").action(() => {
817
906
  info("Setting up pre-commit hook...");
818
907
  try {
@@ -948,6 +1037,37 @@ program.command("scan-all [dir]").description("Scan an entire directory or codeb
948
1037
  process.exit(1);
949
1038
  }
950
1039
  });
1040
+ program.command("scan-history").description("Scan Git commit history for exposed secrets (defaults to last commit)").option("--show-sol", "Show solutions for vulnerabilities").option("--since <date>", 'Scan commits since a specific date (e.g. "30 days ago")').option("--id <hash>", "Scan a specific commit hash").option("--all", "Scan all branches and history").action(async (options) => {
1041
+ info("Git CLI Scanner (History Mode) running...");
1042
+ try {
1043
+ const spinnerMsgs = [
1044
+ "Rewinding Git history...",
1045
+ "Extracting historical diffs...",
1046
+ "Scanning temporal anomalies...",
1047
+ "Looking for buried secrets...",
1048
+ "Thinking..."
1049
+ ];
1050
+ const spinner = new Spinner(spinnerMsgs);
1051
+ spinner.start();
1052
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1053
+ const issues = await scanHistoryDiffs({
1054
+ since: options.since,
1055
+ id: options.id,
1056
+ all: options.all
1057
+ });
1058
+ if (issues.length > 0) {
1059
+ const blockerIssues = issues.filter((i) => i.severity !== "dummy");
1060
+ spinner.fail(`Found ${issues.length} historical vulnerabilities! (${blockerIssues.length} blockers)`);
1061
+ const { printIssues: printIssues2 } = (init_logger(), __toCommonJS(logger_exports));
1062
+ printIssues2(issues, options.showSol);
1063
+ } else {
1064
+ spinner.stop("No vulnerabilities found in the scanned history!");
1065
+ }
1066
+ } catch (err) {
1067
+ error(`History scan failed: ${err.message}`);
1068
+ process.exit(1);
1069
+ }
1070
+ });
951
1071
  program.command("explore").description("Launch an interactive Terminal UI to browse and scan files").action(async () => {
952
1072
  try {
953
1073
  const { runExplorer: runExplorer2 } = (init_explorer(), __toCommonJS(explorer_exports));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-cli-scanner",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "A powerful interactive CLI tool that scans your codebase for hardcoded secrets, API keys, passwords, and private keys before they reach your Git history.",
5
5
  "main": "dist/cli.js",
6
6
  "scripts": {
package/readme.md CHANGED
@@ -39,7 +39,7 @@ After running `init`, every time you run `git commit`, the scanner will automati
39
39
  | `scan` | Manually scan staged files |
40
40
  | `scan --show-sol` | Scan staged files and show suggested fixes |
41
41
  | `scan-all [dir]` | Scan an entire directory recursively |
42
- | `scan-all [dir] --show-sol` | Scan a directory and show suggested fixes |
42
+ | `scan-history` | Scan Git commit history for leaked secrets |
43
43
  | `explore` | Launch interactive file explorer TUI |
44
44
 
45
45
  ### Enable automatic scanning
@@ -73,6 +73,22 @@ npx git-cli-scanner scan-all ./src
73
73
  npx git-cli-scanner scan-all tests --show-sol
74
74
  ```
75
75
 
76
+ ### Scan Git history (Time Travel)
77
+
78
+ ```bash
79
+ # Scan the very last commit (default)
80
+ npx git-cli-scanner scan-history
81
+
82
+ # Scan a specific commit by hash
83
+ npx git-cli-scanner scan-history --id <hash>
84
+
85
+ # Scan all commits in the last 30 days
86
+ npx git-cli-scanner scan-history --since="30 days ago"
87
+
88
+ # Scan the entire Git history across all branches!
89
+ npx git-cli-scanner scan-history --all
90
+ ```
91
+
76
92
  ### Interactive file explorer
77
93
 
78
94
  ```bash