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.
- package/dist/cli.js +829 -0
- package/package.json +33 -0
- 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
|