git-cli-scanner 1.0.0

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 +829 -0
  2. package/package.json +33 -0
  3. package/readme.md +131 -0
package/dist/cli.js ADDED
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/utils/git.ts
35
+ async function getStagedDiff() {
36
+ try {
37
+ const { stdout: filesOutput } = await execAsync("git diff --cached --name-only");
38
+ const files = filesOutput.trim().split("\n").filter(Boolean);
39
+ if (files.length === 0) {
40
+ return [];
41
+ }
42
+ const diffs = [];
43
+ for (const file of files) {
44
+ const { stdout: diffContent } = await execAsync(`git diff --cached -- "${file}"`);
45
+ diffs.push({ file, content: diffContent });
46
+ }
47
+ return diffs;
48
+ } catch (error2) {
49
+ console.warn("Could not get git diff. Are you in a git repository?");
50
+ return [];
51
+ }
52
+ }
53
+ var import_child_process, import_util, execAsync;
54
+ var init_git = __esm({
55
+ "src/utils/git.ts"() {
56
+ "use strict";
57
+ import_child_process = require("child_process");
58
+ import_util = require("util");
59
+ execAsync = (0, import_util.promisify)(import_child_process.exec);
60
+ }
61
+ });
62
+
63
+ // src/scans/apiKeys.ts
64
+ var rules, apiKeyScanner;
65
+ var init_apiKeys = __esm({
66
+ "src/scans/apiKeys.ts"() {
67
+ "use strict";
68
+ rules = [
69
+ {
70
+ id: "stripe-key",
71
+ description: "Stripe Secret/Restricted Key",
72
+ pattern: /(?:sk|rk)_(?:test|live)_[0-9a-zA-Z]{24}/
73
+ },
74
+ {
75
+ id: "sendgrid-key",
76
+ description: "SendGrid API Key",
77
+ pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/
78
+ },
79
+ {
80
+ id: "mailgun-key",
81
+ description: "Mailgun API Key",
82
+ pattern: /key-[0-9a-zA-Z]{32}/
83
+ },
84
+ {
85
+ id: "twilio-key",
86
+ description: "Twilio API Key",
87
+ pattern: /SK[0-9a-fA-F]{32}/
88
+ },
89
+ {
90
+ id: "generic-api-key",
91
+ description: "Generic API Key / Secret / Password / Passphrase",
92
+ pattern: /(?:api[_\-]?key|secret|token|password|pwd|passphrase)[\s:=]+["'][a-zA-Z0-9\-_!@#$%^&*()=+]{8,}["']/i
93
+ },
94
+ {
95
+ id: "generic-bearer-token",
96
+ description: "Generic Bearer Token",
97
+ pattern: /bearer\s+[a-zA-Z0-9\-_.]{20,}/i
98
+ }
99
+ ];
100
+ apiKeyScanner = {
101
+ id: "api-keys",
102
+ scan(diff) {
103
+ const issues = [];
104
+ const lines = diff.content.split("\n");
105
+ let lineNumber = 1;
106
+ for (const line of lines) {
107
+ if (line.startsWith("+")) {
108
+ const cleanLine = line.substring(1);
109
+ for (const rule of rules) {
110
+ if (rule.pattern.test(cleanLine)) {
111
+ issues.push({
112
+ type: rule.id,
113
+ file: diff.file,
114
+ line: lineNumber,
115
+ match: cleanLine.trim().substring(0, 50) + "...",
116
+ severity: "high",
117
+ solution: "Store this key in a secret manager or environment variable. Never hardcode it."
118
+ });
119
+ }
120
+ }
121
+ }
122
+ lineNumber++;
123
+ }
124
+ return issues;
125
+ }
126
+ };
127
+ }
128
+ });
129
+
130
+ // src/scans/cloudProviders.ts
131
+ var rules2, cloudProviderScanner;
132
+ var init_cloudProviders = __esm({
133
+ "src/scans/cloudProviders.ts"() {
134
+ "use strict";
135
+ rules2 = [
136
+ {
137
+ id: "aws-access-key",
138
+ description: "AWS Access Key ID",
139
+ pattern: /(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/
140
+ },
141
+ {
142
+ id: "aws-secret-key",
143
+ description: "AWS Secret Access Key (heuristics)",
144
+ pattern: /aws_?(?:secret)?_?(?:access)?_?key[\s:=]+["'][a-zA-Z0-9\/+]{40}["']/i
145
+ },
146
+ {
147
+ id: "gcp-api-key",
148
+ description: "Google Cloud API Key",
149
+ pattern: /AIza[0-9A-Za-z\-_]{35}/
150
+ }
151
+ ];
152
+ cloudProviderScanner = {
153
+ id: "cloud-providers",
154
+ scan(diff) {
155
+ const issues = [];
156
+ const lines = diff.content.split("\n");
157
+ let lineNumber = 1;
158
+ for (const line of lines) {
159
+ if (line.startsWith("+")) {
160
+ const cleanLine = line.substring(1);
161
+ for (const rule of rules2) {
162
+ if (rule.pattern.test(cleanLine)) {
163
+ issues.push({
164
+ type: rule.id,
165
+ file: diff.file,
166
+ line: lineNumber,
167
+ match: cleanLine.trim().substring(0, 50) + "...",
168
+ severity: "high",
169
+ solution: "Move this cloud credential to IAM roles, Vault, or environment variables."
170
+ });
171
+ }
172
+ }
173
+ }
174
+ lineNumber++;
175
+ }
176
+ return issues;
177
+ }
178
+ };
179
+ }
180
+ });
181
+
182
+ // src/scans/collaboration.ts
183
+ var rules3, collaborationScanner;
184
+ var init_collaboration = __esm({
185
+ "src/scans/collaboration.ts"() {
186
+ "use strict";
187
+ rules3 = [
188
+ {
189
+ id: "github-pat",
190
+ description: "GitHub Personal Access Token",
191
+ pattern: /ghp_[a-zA-Z0-9]{36}/
192
+ },
193
+ {
194
+ id: "github-oauth",
195
+ description: "GitHub OAuth Access Token",
196
+ pattern: /gho_[a-zA-Z0-9]{36}/
197
+ },
198
+ {
199
+ id: "slack-token",
200
+ description: "Slack Token",
201
+ pattern: /xox[pboar]-[a-zA-Z0-9]{10,13}-[a-zA-Z0-9]{10,13}-[a-zA-Z0-9]{10,13}-[a-zA-Z0-9]{32}/
202
+ },
203
+ {
204
+ id: "slack-webhook",
205
+ description: "Slack Webhook",
206
+ pattern: /https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8,10}\/B[a-zA-Z0-9_]{8,10}\/[a-zA-Z0-9_]{24}/
207
+ }
208
+ ];
209
+ collaborationScanner = {
210
+ id: "collaboration",
211
+ scan(diff) {
212
+ const issues = [];
213
+ const lines = diff.content.split("\n");
214
+ let lineNumber = 1;
215
+ for (const line of lines) {
216
+ if (line.startsWith("+")) {
217
+ const cleanLine = line.substring(1);
218
+ for (const rule of rules3) {
219
+ if (rule.pattern.test(cleanLine)) {
220
+ issues.push({
221
+ type: rule.id,
222
+ file: diff.file,
223
+ line: lineNumber,
224
+ match: cleanLine.trim().substring(0, 50) + "...",
225
+ severity: "high",
226
+ solution: "Use OAuth tokens or environment variables. Do not commit team tokens."
227
+ });
228
+ }
229
+ }
230
+ }
231
+ lineNumber++;
232
+ }
233
+ return issues;
234
+ }
235
+ };
236
+ }
237
+ });
238
+
239
+ // src/scans/privateKeys.ts
240
+ var rules4, privateKeyScanner;
241
+ var init_privateKeys = __esm({
242
+ "src/scans/privateKeys.ts"() {
243
+ "use strict";
244
+ rules4 = [
245
+ {
246
+ id: "rsa-private-key",
247
+ description: "RSA Private Key",
248
+ pattern: /-----BEGIN RSA PRIVATE KEY-----/
249
+ },
250
+ {
251
+ id: "openssh-private-key",
252
+ description: "OpenSSH Private Key",
253
+ pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/
254
+ },
255
+ {
256
+ id: "pgp-private-key",
257
+ description: "PGP Private Key",
258
+ pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/
259
+ }
260
+ ];
261
+ privateKeyScanner = {
262
+ id: "private-keys",
263
+ scan(diff) {
264
+ const issues = [];
265
+ const lines = diff.content.split("\n");
266
+ let lineNumber = 1;
267
+ for (const line of lines) {
268
+ if (line.startsWith("+")) {
269
+ const cleanLine = line.substring(1);
270
+ for (const rule of rules4) {
271
+ if (rule.pattern.test(cleanLine)) {
272
+ issues.push({
273
+ type: rule.id,
274
+ file: diff.file,
275
+ line: lineNumber,
276
+ match: cleanLine.trim().substring(0, 50) + "...",
277
+ severity: "high",
278
+ solution: "Never commit private keys. Revoke this key immediately if it was ever public, and use a secure key manager."
279
+ });
280
+ }
281
+ }
282
+ }
283
+ lineNumber++;
284
+ }
285
+ return issues;
286
+ }
287
+ };
288
+ }
289
+ });
290
+
291
+ // src/scans/envFiles.ts
292
+ var envFileScanner;
293
+ var init_envFiles = __esm({
294
+ "src/scans/envFiles.ts"() {
295
+ "use strict";
296
+ envFileScanner = {
297
+ id: "banned-files",
298
+ scan(diff) {
299
+ const issues = [];
300
+ const filename = diff.file.split("/").pop() || "";
301
+ const bannedExtensions = [".pem", ".key", ".sqlite", ".db", ".log", ".p12", ".pfx"];
302
+ const isEnvFile = /(^|\/)\.env(\..+)?$/.test(diff.file);
303
+ const isNodeModules = diff.file.startsWith("node_modules/");
304
+ if (isEnvFile || isNodeModules || bannedExtensions.some((ext) => diff.file.endsWith(ext))) {
305
+ let isIgnored = false;
306
+ const fs4 = require("fs");
307
+ const path4 = require("path");
308
+ try {
309
+ const gitIgnorePath = path4.join(process.cwd(), ".gitignore");
310
+ if (fs4.existsSync(gitIgnorePath)) {
311
+ const gitIgnore = fs4.readFileSync(gitIgnorePath, "utf8");
312
+ if (gitIgnore.includes(".env") || gitIgnore.includes(filename) || gitIgnore.includes(diff.file)) {
313
+ isIgnored = true;
314
+ }
315
+ }
316
+ const npmIgnorePath = path4.join(process.cwd(), ".npmignore");
317
+ if (fs4.existsSync(npmIgnorePath)) {
318
+ const npmIgnore = fs4.readFileSync(npmIgnorePath, "utf8");
319
+ if (npmIgnore.includes(".env") || npmIgnore.includes(filename) || npmIgnore.includes(diff.file)) {
320
+ isIgnored = true;
321
+ }
322
+ }
323
+ } catch (e) {
324
+ }
325
+ const matchMsg = isIgnored ? `File extension/name matched banned list (but is currently in .gitignore)` : `DANGER: ${filename} is NOT in .gitignore or .npmignore!`;
326
+ const severity = isIgnored ? "dummy" : "medium";
327
+ issues.push({
328
+ type: "banned-file-type",
329
+ file: diff.file,
330
+ line: 0,
331
+ match: matchMsg,
332
+ severity,
333
+ solution: "Add this file to .gitignore. If it is a template, rename it to .env.example."
334
+ });
335
+ }
336
+ return issues;
337
+ }
338
+ };
339
+ }
340
+ });
341
+
342
+ // src/scans/index.ts
343
+ var allScanners;
344
+ var init_scans = __esm({
345
+ "src/scans/index.ts"() {
346
+ "use strict";
347
+ init_apiKeys();
348
+ init_cloudProviders();
349
+ init_collaboration();
350
+ init_privateKeys();
351
+ init_envFiles();
352
+ allScanners = [
353
+ apiKeyScanner,
354
+ cloudProviderScanner,
355
+ collaborationScanner,
356
+ privateKeyScanner,
357
+ envFileScanner
358
+ ];
359
+ }
360
+ });
361
+
362
+ // src/utils/fs.ts
363
+ function walkDirectory(dir, baseDir = dir) {
364
+ let results = [];
365
+ const list = fs.readdirSync(dir);
366
+ for (const file of list) {
367
+ const fullPath = path.join(dir, file);
368
+ if (file === ".git" || file === "node_modules" || file === "dist" || file === "build") {
369
+ continue;
370
+ }
371
+ const stat = fs.statSync(fullPath);
372
+ if (stat && stat.isDirectory()) {
373
+ results = results.concat(walkDirectory(fullPath, baseDir));
374
+ } else {
375
+ try {
376
+ const content = fs.readFileSync(fullPath, "utf8");
377
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
378
+ results.push({ file: relativePath, content });
379
+ } catch (err) {
380
+ }
381
+ }
382
+ }
383
+ return results;
384
+ }
385
+ var fs, path;
386
+ var init_fs = __esm({
387
+ "src/utils/fs.ts"() {
388
+ "use strict";
389
+ fs = __toESM(require("fs"));
390
+ path = __toESM(require("path"));
391
+ }
392
+ });
393
+
394
+ // src/utils/dummy.ts
395
+ function isDummySecret(match) {
396
+ const lowerMatch = match.toLowerCase();
397
+ const dummyWords = [
398
+ "example",
399
+ "dummy",
400
+ "test",
401
+ "fake",
402
+ "demo",
403
+ "sample",
404
+ "your_",
405
+ "insert_",
406
+ "replace_",
407
+ "placeholder",
408
+ "my-secret",
409
+ "SOME_LONG_TOKEN"
410
+ ];
411
+ for (const word of dummyWords) {
412
+ if (lowerMatch.includes(word)) {
413
+ return true;
414
+ }
415
+ }
416
+ const knownDummies = [
417
+ "AKIAIOSFODNN7EXAMPLE",
418
+ // AWS Test Key
419
+ "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
420
+ // AWS Test Secret
421
+ ];
422
+ for (const known of knownDummies) {
423
+ if (match.includes(known)) {
424
+ return true;
425
+ }
426
+ }
427
+ const lowEntropyRegex = /(123456789|abcdef|000000|111111|xxxxxx)/i;
428
+ if (lowEntropyRegex.test(lowerMatch)) {
429
+ return true;
430
+ }
431
+ return false;
432
+ }
433
+ var init_dummy = __esm({
434
+ "src/utils/dummy.ts"() {
435
+ "use strict";
436
+ }
437
+ });
438
+
439
+ // src/scanner/index.ts
440
+ var scanner_exports = {};
441
+ __export(scanner_exports, {
442
+ scanDiff: () => scanDiff,
443
+ scanDirectory: () => scanDirectory
444
+ });
445
+ async function scanDiff() {
446
+ const diffs = await getStagedDiff();
447
+ const issues = [];
448
+ for (const diff of diffs) {
449
+ for (const scanner of allScanners) {
450
+ issues.push(...scanner.scan(diff));
451
+ }
452
+ }
453
+ return issues;
454
+ }
455
+ async function scanDirectory(dirPath) {
456
+ const files = walkDirectory(dirPath);
457
+ const issues = [];
458
+ for (const fileData of files) {
459
+ const mockContent = fileData.content.split("\n").map((line) => "+" + line).join("\n");
460
+ const mockDiff = {
461
+ file: fileData.file,
462
+ content: mockContent
463
+ };
464
+ for (const scanner of allScanners) {
465
+ issues.push(...scanner.scan(mockDiff));
466
+ }
467
+ }
468
+ for (const issue of issues) {
469
+ if (isDummySecret(issue.match)) {
470
+ issue.severity = "dummy";
471
+ }
472
+ }
473
+ return issues;
474
+ }
475
+ var init_scanner = __esm({
476
+ "src/scanner/index.ts"() {
477
+ "use strict";
478
+ init_git();
479
+ init_scans();
480
+ init_fs();
481
+ init_dummy();
482
+ }
483
+ });
484
+
485
+ // src/utils/logger.ts
486
+ var logger_exports = {};
487
+ __export(logger_exports, {
488
+ error: () => error,
489
+ info: () => info,
490
+ printIssues: () => printIssues,
491
+ success: () => success
492
+ });
493
+ function info(message) {
494
+ console.log(import_picocolors.default.blue("\u2139 info ") + message);
495
+ }
496
+ function success(message) {
497
+ console.log(import_picocolors.default.green("\u2714 success ") + message);
498
+ }
499
+ function error(message) {
500
+ console.error(import_picocolors.default.red("\u2716 error ") + message);
501
+ }
502
+ function printIssues(issues, showSolution = false) {
503
+ console.error("\n" + import_picocolors.default.bold("Scan Results:"));
504
+ issues.forEach((issue) => {
505
+ let indicator = "\u25CF";
506
+ let label = "HIGH";
507
+ let colorFn = import_picocolors.default.red;
508
+ if (issue.severity === "medium") {
509
+ label = "MEDIUM";
510
+ colorFn = import_picocolors.default.yellow;
511
+ } else if (issue.severity === "dummy") {
512
+ indicator = "\u25CB";
513
+ label = "IGNORED (DUMMY)";
514
+ colorFn = import_picocolors.default.dim;
515
+ }
516
+ let displayMatch = issue.match.replace(/\n/g, " ").trim();
517
+ if (displayMatch.length > 60) {
518
+ displayMatch = displayMatch.substring(0, 57) + "...";
519
+ }
520
+ console.error(`
521
+ ${colorFn(indicator)} ${colorFn(import_picocolors.default.bold(label))} ${import_picocolors.default.dim("\xB7")} ${issue.type}`);
522
+ console.error(` ${import_picocolors.default.dim("File:")} ${issue.file}:${issue.line || "?"}`);
523
+ console.error(` ${import_picocolors.default.dim("Match:")} ${displayMatch}`);
524
+ if (showSolution && issue.solution) {
525
+ console.error(` ${import_picocolors.default.dim("Fix:")} ${import_picocolors.default.green(issue.solution)}`);
526
+ }
527
+ });
528
+ console.error();
529
+ }
530
+ var import_picocolors;
531
+ var init_logger = __esm({
532
+ "src/utils/logger.ts"() {
533
+ "use strict";
534
+ import_picocolors = __toESM(require("picocolors"));
535
+ }
536
+ });
537
+
538
+ // src/utils/explorer.ts
539
+ var explorer_exports = {};
540
+ __export(explorer_exports, {
541
+ explorePrompt: () => explorePrompt
542
+ });
543
+ var import_core, import_picocolors3, fs2, path2, explorePrompt;
544
+ var init_explorer = __esm({
545
+ "src/utils/explorer.ts"() {
546
+ "use strict";
547
+ import_core = require("@inquirer/core");
548
+ import_picocolors3 = __toESM(require("picocolors"));
549
+ fs2 = __toESM(require("fs"));
550
+ path2 = __toESM(require("path"));
551
+ init_scanner();
552
+ explorePrompt = (0, import_core.createPrompt)(
553
+ (config, done) => {
554
+ const [currentPath, setCurrentPath] = (0, import_core.useState)(config.currentDir);
555
+ const [selectedIndex, setSelectedIndex] = (0, import_core.useState)(0);
556
+ const [scanResult, setScanResult] = (0, import_core.useState)(null);
557
+ const [scanningFile, setScanningFile] = (0, import_core.useState)(null);
558
+ const prefix = (0, import_core.usePrefix)({ status: "idle" });
559
+ let items = [];
560
+ try {
561
+ items = fs2.readdirSync(currentPath, { withFileTypes: true });
562
+ } catch (err) {
563
+ }
564
+ items.sort((a, b) => {
565
+ if (a.isDirectory() && !b.isDirectory()) return -1;
566
+ if (!a.isDirectory() && b.isDirectory()) return 1;
567
+ return a.name.localeCompare(b.name);
568
+ });
569
+ const choices = [
570
+ { name: "\u{1F519} Go Back (or press Left Arrow)", type: "back" },
571
+ ...items.map((i) => ({
572
+ name: i.isDirectory() ? `\u{1F4C1} ${i.name}` : `\u{1F4C4} ${i.name}`,
573
+ type: i.isDirectory() ? "dir" : "file",
574
+ nameRaw: i.name
575
+ }))
576
+ ];
577
+ (0, import_core.useKeypress)(async (key, rl) => {
578
+ if ((0, import_core.isEnterKey)(key) || key.name === "right") {
579
+ const selected = choices[selectedIndex];
580
+ if (selected.type === "back") {
581
+ setCurrentPath(path2.dirname(currentPath));
582
+ setSelectedIndex(0);
583
+ setScanResult(null);
584
+ } else if (selected.type === "dir") {
585
+ setCurrentPath(path2.join(currentPath, selected.nameRaw));
586
+ setSelectedIndex(0);
587
+ setScanResult(null);
588
+ } else if (selected.type === "file") {
589
+ const fullPath = path2.join(currentPath, selected.nameRaw);
590
+ setScanningFile(fullPath);
591
+ const issues = await scanDirectory(currentPath);
592
+ const fileIssues = issues.filter((i) => i.file === path2.relative(process.cwd(), fullPath).replace(/\\/g, "/"));
593
+ setScanResult(fileIssues);
594
+ setScanningFile(null);
595
+ }
596
+ } else if (key.name === "left") {
597
+ setCurrentPath(path2.dirname(currentPath));
598
+ setSelectedIndex(0);
599
+ setScanResult(null);
600
+ } else if ((0, import_core.isUpKey)(key)) {
601
+ setSelectedIndex((prev) => prev > 0 ? prev - 1 : choices.length - 1);
602
+ } else if ((0, import_core.isDownKey)(key)) {
603
+ setSelectedIndex((prev) => prev < choices.length - 1 ? prev + 1 : 0);
604
+ } else if (key.name === "c" && key.ctrl) {
605
+ done("");
606
+ }
607
+ });
608
+ let message = `${prefix} ${import_picocolors3.default.bold("Exploring:")} ${import_picocolors3.default.cyan(currentPath)}
609
+
610
+ `;
611
+ const startIndex = Math.max(0, selectedIndex - 10);
612
+ const endIndex = Math.min(choices.length, startIndex + 20);
613
+ for (let i = startIndex; i < endIndex; i++) {
614
+ const choice = choices[i];
615
+ if (i === selectedIndex) {
616
+ message += import_picocolors3.default.cyan(`\u276F ${choice.name}
617
+ `);
618
+ } else {
619
+ message += ` ${choice.name}
620
+ `;
621
+ }
622
+ }
623
+ if (scanResult) {
624
+ message += `
625
+ ${import_picocolors3.default.bold("Scan Results for " + choices[selectedIndex].nameRaw + ":")}
626
+ `;
627
+ if (scanResult.length === 0) {
628
+ message += import_picocolors3.default.green("\u2714 No vulnerabilities found in this file.\n");
629
+ } else {
630
+ scanResult.forEach((issue) => {
631
+ message += ` ${issue.severity === "high" ? import_picocolors3.default.red("\u25CF HIGH") : issue.severity === "medium" ? import_picocolors3.default.yellow("\u25CF MEDIUM") : import_picocolors3.default.dim("\u25CB DUMMY")} \xB7 ${issue.type}
632
+ `;
633
+ message += ` Fix: ${import_picocolors3.default.green(issue.solution || "No solution provided")}
634
+ `;
635
+ });
636
+ }
637
+ } else if (scanningFile) {
638
+ message += `
639
+ ${import_picocolors3.default.yellow("Scanning...")} (Press Right Arrow to scan)
640
+ `;
641
+ }
642
+ return message;
643
+ }
644
+ );
645
+ }
646
+ });
647
+
648
+ // src/cli.ts
649
+ var import_commander = require("commander");
650
+ init_scanner();
651
+ init_logger();
652
+
653
+ // src/utils/prompts.ts
654
+ var import_prompts = require("@inquirer/prompts");
655
+ async function askToScan() {
656
+ return await (0, import_prompts.confirm)({
657
+ message: "Do you want to scan for vulnerabilities before committing?",
658
+ default: true
659
+ });
660
+ }
661
+ async function askToContinue() {
662
+ return await (0, import_prompts.confirm)({
663
+ message: "Vulnerabilities were found! Do you still want to continue with the commit?",
664
+ default: false
665
+ });
666
+ }
667
+
668
+ // src/utils/spinner.ts
669
+ var import_picocolors2 = __toESM(require("picocolors"));
670
+ var Spinner = class {
671
+ timer = null;
672
+ messages;
673
+ currentMessageIndex = 0;
674
+ // Cute orange flower animation frames
675
+ frames = ["\u273F", "\u2740", "\u2741", "\u2742", "\u2743", "\u{1F3F5}"];
676
+ currentFrame = 0;
677
+ ticks = 0;
678
+ constructor(messages) {
679
+ this.messages = Array.isArray(messages) ? messages : [messages];
680
+ }
681
+ start() {
682
+ process.stdout.write("\x1B[?25l");
683
+ this.timer = setInterval(() => {
684
+ if (this.ticks > 0 && this.ticks % 15 === 0) {
685
+ this.currentMessageIndex = (this.currentMessageIndex + 1) % this.messages.length;
686
+ }
687
+ const currentMessage = this.messages[this.currentMessageIndex];
688
+ process.stdout.write(`\r\x1B[K${import_picocolors2.default.yellow(this.frames[this.currentFrame])} ${currentMessage}`);
689
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
690
+ this.ticks++;
691
+ }, 120);
692
+ }
693
+ stop(successMessage) {
694
+ if (this.timer) clearInterval(this.timer);
695
+ process.stdout.write("\r\x1B[K");
696
+ process.stdout.write("\x1B[?25h");
697
+ if (successMessage) {
698
+ console.log(`${import_picocolors2.default.green("\u2714")} ${successMessage}`);
699
+ }
700
+ }
701
+ fail(failMessage) {
702
+ if (this.timer) clearInterval(this.timer);
703
+ process.stdout.write("\r\x1B[K");
704
+ process.stdout.write("\x1B[?25h");
705
+ if (failMessage) {
706
+ console.log(`${import_picocolors2.default.red("\u2716")} ${failMessage}`);
707
+ }
708
+ }
709
+ };
710
+
711
+ // src/cli.ts
712
+ var import_child_process2 = require("child_process");
713
+ var fs3 = __toESM(require("fs"));
714
+ var path3 = __toESM(require("path"));
715
+ var program = new import_commander.Command();
716
+ program.name("git-cli-scanner").description("Interactive Git hooks vulnerability scanner").version("1.0.0");
717
+ program.command("init").description("Initialize husky and add the pre-commit hook automatically").action(() => {
718
+ info("Setting up git hooks with husky...");
719
+ try {
720
+ (0, import_child_process2.execSync)("npx husky init", { stdio: "inherit" });
721
+ const hookPath = path3.join(process.cwd(), ".husky", "pre-commit");
722
+ const hookContent = `
723
+ # exec < /dev/tty is required to allow interactive prompts in git hooks
724
+ exec < /dev/tty
725
+ npx git-cli-scanner scan
726
+ `.trim();
727
+ fs3.writeFileSync(hookPath, hookContent, "utf-8");
728
+ success("Successfully installed pre-commit hook!");
729
+ info("Next time you run `git commit`, the scanner will prompt you.");
730
+ } catch (err) {
731
+ error(`Failed to initialize: ${err.message}`);
732
+ }
733
+ });
734
+ program.command("scan").description("Interactive vulnerability scan for staged files").option("--show-sol", "Show solutions for vulnerabilities").action(async (options) => {
735
+ info("GitHub CLI Scanner triggered by Git Hook.");
736
+ try {
737
+ const shouldScan = await askToScan();
738
+ if (!shouldScan) {
739
+ info("Skipping scan as requested. Proceeding with commit...");
740
+ process.exit(0);
741
+ }
742
+ const spinner = new Spinner([
743
+ "Scanning staged files for vulnerabilities...",
744
+ "Analyzing code patterns...",
745
+ "Checking for exposed API keys...",
746
+ "Inspecting hidden files and directories...",
747
+ "Thinking..."
748
+ ]);
749
+ spinner.start();
750
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
751
+ const issues = await scanDiff();
752
+ if (issues.length > 0) {
753
+ const blockerIssues = issues.filter((i) => i.severity !== "dummy");
754
+ spinner.fail(`Found ${issues.length} potential vulnerabilities! (${blockerIssues.length} blockers)`);
755
+ const { printIssues: printIssues2 } = (init_logger(), __toCommonJS(logger_exports));
756
+ printIssues2(issues, options.showSol);
757
+ if (blockerIssues.length === 0) {
758
+ info("No high or medium vulnerabilities found. Safe to commit!");
759
+ process.exit(0);
760
+ }
761
+ const shouldContinue = await askToContinue();
762
+ if (shouldContinue) {
763
+ info("Proceeding with commit despite vulnerabilities.");
764
+ process.exit(0);
765
+ } else {
766
+ error("Commit aborted. Please edit your files and try again.");
767
+ process.exit(1);
768
+ }
769
+ } else {
770
+ spinner.stop("No vulnerabilities found. Safe to commit!");
771
+ process.exit(0);
772
+ }
773
+ } catch (err) {
774
+ if (err.name === "ExitPromptError" || err.message?.includes("closed")) {
775
+ error("Scan aborted by user.");
776
+ process.exit(1);
777
+ }
778
+ error(`Scanner failed: ${err.message}`);
779
+ process.exit(1);
780
+ }
781
+ });
782
+ program.command("scan-all [dir]").description("Scan an entire directory or codebase for vulnerabilities").option("--show-sol", "Show solutions for vulnerabilities").action(async (dir, options) => {
783
+ const scanDir = dir || process.cwd();
784
+ info(`Scanning directory: ${scanDir}`);
785
+ try {
786
+ const spinner = new Spinner([
787
+ "Walking directory and reading files...",
788
+ "Analyzing code patterns...",
789
+ "Checking for exposed API keys...",
790
+ "Inspecting hidden files and directories...",
791
+ "Thinking..."
792
+ ]);
793
+ spinner.start();
794
+ const { scanDirectory: scanDirectory2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
795
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
796
+ const issues = await scanDirectory2(scanDir);
797
+ if (issues.length > 0) {
798
+ const blockerIssues = issues.filter((i) => i.severity !== "dummy");
799
+ spinner.fail(`Found ${issues.length} potential vulnerabilities! (${blockerIssues.length} blockers)`);
800
+ const { printIssues: printIssues2 } = (init_logger(), __toCommonJS(logger_exports));
801
+ printIssues2(issues, options.showSol);
802
+ if (blockerIssues.length > 0) {
803
+ process.exit(1);
804
+ } else {
805
+ process.exit(0);
806
+ }
807
+ } else {
808
+ spinner.stop("No vulnerabilities found. Directory is safe!");
809
+ process.exit(0);
810
+ }
811
+ } catch (err) {
812
+ error(`Scanner failed: ${err.message}`);
813
+ process.exit(1);
814
+ }
815
+ });
816
+ program.command("explore").description("Launch an interactive Terminal UI to browse and scan files").action(async () => {
817
+ try {
818
+ const { explorePrompt: explorePrompt2 } = (init_explorer(), __toCommonJS(explorer_exports));
819
+ await explorePrompt2({ currentDir: process.cwd() });
820
+ process.exit(0);
821
+ } catch (err) {
822
+ if (err.name === "ExitPromptError" || err.message?.includes("closed")) {
823
+ process.exit(0);
824
+ }
825
+ error(`Explorer failed: ${err.message}`);
826
+ process.exit(1);
827
+ }
828
+ });
829
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "git-cli-scanner",
3
+ "version": "1.0.0",
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
+ "main": "dist/cli.js",
6
+ "scripts": {
7
+ "test": "vitest run",
8
+ "build": "tsup src/cli.ts --format cjs",
9
+ "prepare": "husky"
10
+ },
11
+ "keywords": ["security", "scanner", "git", "secrets", "api-keys", "cli", "pre-commit", "vulnerability"],
12
+ "author": "riskchips",
13
+ "license": "ISC",
14
+ "dependencies": {
15
+ "@inquirer/core": "^10.3.2",
16
+ "@inquirer/prompts": "^7.10.1",
17
+ "commander": "^15.0.0",
18
+ "husky": "^9.1.7",
19
+ "picocolors": "^1.1.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^26.4.1",
23
+ "tsup": "^8.5.1",
24
+ "typescript": "^7.0.2",
25
+ "vitest": "^5.0.0"
26
+ },
27
+ "bin": {
28
+ "git-cli-scanner": "dist/cli.js"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ]
33
+ }
package/readme.md ADDED
@@ -0,0 +1,131 @@
1
+ # Git CLI Scanner
2
+
3
+ A powerful, interactive CLI tool that scans your codebase for hardcoded secrets, API keys, passwords, private keys, and other vulnerabilities before they ever reach your Git history.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g git-cli-scanner
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Set up the pre-commit hook (one-time)
17
+ npx git-cli-scanner init
18
+
19
+ # Now every time you run `git commit`, the scanner will automatically prompt you.
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Commands
25
+
26
+ ### `init` — Set up Git hooks
27
+
28
+ Installs a Husky pre-commit hook that triggers the scanner automatically before every commit.
29
+
30
+ ```bash
31
+ npx git-cli-scanner init
32
+ ```
33
+
34
+ ### `scan` — Scan staged files (Git hook mode)
35
+
36
+ Interactively scans only the files you have staged (`git add`) for vulnerabilities. This is what the pre-commit hook runs.
37
+
38
+ ```bash
39
+ npx git-cli-scanner scan
40
+ npx git-cli-scanner scan --show-sol # Also show suggested fixes
41
+ ```
42
+
43
+ ### `scan-all` — Scan an entire directory
44
+
45
+ Recursively walks through a directory and scans every file for vulnerabilities. Skips `.git`, `node_modules`, `dist`, and `build` directories automatically.
46
+
47
+ ```bash
48
+ npx git-cli-scanner scan-all . # Scan current directory
49
+ npx git-cli-scanner scan-all ./src # Scan only src/
50
+ npx git-cli-scanner scan-all tests --show-sol # Scan tests/ with solutions
51
+ ```
52
+
53
+ ### `explore` — Interactive file explorer TUI
54
+
55
+ Launch a fully interactive Terminal UI to browse your project and scan files on the fly.
56
+
57
+ ```bash
58
+ npx git-cli-scanner explore
59
+ ```
60
+
61
+ **Controls:**
62
+ | Key | Action |
63
+ |-----|--------|
64
+ | `Up / Down` | Move cursor through files and folders |
65
+ | `Right / Enter` | Open a folder or scan a file |
66
+ | `Left` | Go back to parent directory |
67
+ | `Ctrl+C` | Exit the explorer |
68
+
69
+ ---
70
+
71
+ ## Flags
72
+
73
+ | Flag | Available On | Description |
74
+ |------|-------------|-------------|
75
+ | `--show-sol` | `scan`, `scan-all` | Show actionable fix suggestions for each vulnerability |
76
+
77
+ ---
78
+
79
+ ## What It Detects
80
+
81
+ ### HIGH Severity (Red)
82
+ - AWS Access Keys & Secret Keys
83
+ - Google Cloud API Keys
84
+ - Slack Tokens & Webhooks
85
+ - GitHub Personal Access Tokens & OAuth Tokens
86
+ - Stripe API Keys
87
+ - SendGrid, Mailgun, and Twilio Tokens
88
+ - RSA, OpenSSH, and PGP Private Keys
89
+ - Generic API keys, passwords, and passphrases (any format like `api_key`, `api-key`, `API_KEY`, etc.)
90
+
91
+ ### MEDIUM Severity (Yellow)
92
+ - `.env` files not listed in `.gitignore`
93
+ - Banned file types: `.pem`, `.key`, `.sqlite`, `.db`, `.log`, `.p12`, `.pfx`
94
+ - `node_modules/` committed to the repo
95
+
96
+ ### Dummy Detection (Dimmed)
97
+ The scanner has a strict dummy detection engine that automatically identifies obvious test/example secrets (like `AKIAIOSFODNN7EXAMPLE` or values containing `test`, `dummy`, `sample`, etc.) and downgrades them so they don't block your workflow.
98
+
99
+ ---
100
+
101
+ ## Gitignore Awareness
102
+
103
+ The scanner checks if banned files (like `.env`, `.sqlite`, `.pem`) are listed in your `.gitignore` or `.npmignore`. If they are, the issue is downgraded to a safe "IGNORED" status. If they are NOT, you get a loud warning:
104
+
105
+ ```
106
+ DANGER: .env is NOT in .gitignore or .npmignore!
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Severity Levels
112
+
113
+ | Indicator | Level | Color | Meaning |
114
+ |-----------|-------|-------|---------|
115
+ | `●` | HIGH | Red | Hardcoded secrets that must be removed |
116
+ | `●` | MEDIUM | Yellow | Risky files that should be in .gitignore |
117
+ | `○` | IGNORED (DUMMY) | Dim | Detected but identified as a test/example value |
118
+
119
+ ---
120
+
121
+ ## Running Tests
122
+
123
+ ```bash
124
+ npm test
125
+ ```
126
+
127
+ ---
128
+
129
+ ## License
130
+
131
+ ISC