arnav-audit 1.0.0 → 1.0.2
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/bin/cli.js +155 -32
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -57,6 +57,8 @@ const options = {
|
|
|
57
57
|
prompts: false,
|
|
58
58
|
securityOnly: false,
|
|
59
59
|
leakageOnly: false,
|
|
60
|
+
verbose: false,
|
|
61
|
+
files: false,
|
|
60
62
|
help: false,
|
|
61
63
|
version: false,
|
|
62
64
|
};
|
|
@@ -65,6 +67,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
65
67
|
const arg = args[i];
|
|
66
68
|
if (arg === '--help' || arg === '-h') options.help = true;
|
|
67
69
|
else if (arg === '--version' || arg === '-v') options.version = true;
|
|
70
|
+
else if (arg === '--verbose') options.verbose = true;
|
|
71
|
+
else if (arg === '--files' || arg === '--list') options.files = true;
|
|
68
72
|
else if (arg === '--json') options.json = true;
|
|
69
73
|
else if (arg === '--prompts' || arg === '--fix') options.prompts = true;
|
|
70
74
|
else if (arg === '--security' || arg === '--security-only') options.securityOnly = true;
|
|
@@ -73,7 +77,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
if (options.version) {
|
|
76
|
-
console.log('arnav-audit v1.0.
|
|
80
|
+
console.log('arnav-audit v1.0.2');
|
|
77
81
|
process.exit(0);
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -88,6 +92,8 @@ ${C.bold}TARGETS:${C.reset}
|
|
|
88
92
|
[https://url] Live website URL to run headless CDP telemetry & 200 checks
|
|
89
93
|
|
|
90
94
|
${C.bold}OPTIONS:${C.reset}
|
|
95
|
+
--files, --list List every single scanned file path with inspection status
|
|
96
|
+
--verbose Display detailed scan logs and file-by-file verification
|
|
91
97
|
--prompts, --fix Generate copy-paste AI fix prompts for Cursor & Claude Code
|
|
92
98
|
--security-only Scan exclusively for exposed credentials, secrets & XSS vectors
|
|
93
99
|
--leakage-only Scan exclusively for memory, event listeners & viewport leaks
|
|
@@ -97,6 +103,7 @@ ${C.bold}OPTIONS:${C.reset}
|
|
|
97
103
|
|
|
98
104
|
${C.bold}EXAMPLES:${C.reset}
|
|
99
105
|
$ npx arnav-audit .
|
|
106
|
+
$ npx arnav-audit . --files
|
|
100
107
|
$ npx arnav-audit https://mejor-iota.vercel.app
|
|
101
108
|
$ npx arnav-audit . --prompts
|
|
102
109
|
$ npx arnav-audit . --json > audit-report.json
|
|
@@ -112,18 +119,31 @@ const IGNORED_DIRS = new Set([
|
|
|
112
119
|
'.git',
|
|
113
120
|
'.next',
|
|
114
121
|
'.vercel',
|
|
122
|
+
'.kilo',
|
|
123
|
+
'.pytest_cache',
|
|
124
|
+
'artifacts',
|
|
125
|
+
'test-results',
|
|
126
|
+
'playwright-report',
|
|
115
127
|
'dist',
|
|
116
128
|
'build',
|
|
117
129
|
'coverage',
|
|
118
130
|
'.cache',
|
|
119
131
|
'venv',
|
|
120
132
|
'.venv',
|
|
133
|
+
'env',
|
|
121
134
|
'__pycache__',
|
|
122
135
|
]);
|
|
123
136
|
|
|
124
|
-
const
|
|
125
|
-
'.
|
|
126
|
-
'.
|
|
137
|
+
const BINARY_EXTS = new Set([
|
|
138
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.avif', '.svgz',
|
|
139
|
+
'.mp4', '.webm', '.ogg', '.mp3', '.wav', '.flac',
|
|
140
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
141
|
+
'.zip', '.tar', '.gz', '.tgz', '.rar', '.7z',
|
|
142
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
143
|
+
'.pyc', '.pyo', '.pyd',
|
|
144
|
+
'.db', '.sqlite', '.sqlite3',
|
|
145
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
146
|
+
'.tsbuildinfo'
|
|
127
147
|
]);
|
|
128
148
|
|
|
129
149
|
function walkDir(dir, fileList = []) {
|
|
@@ -135,13 +155,18 @@ function walkDir(dir, fileList = []) {
|
|
|
135
155
|
try {
|
|
136
156
|
const stat = fs.statSync(fullPath);
|
|
137
157
|
if (stat.isDirectory()) {
|
|
158
|
+
// Allow .github workflow directory while ignoring other hidden dot-folders
|
|
159
|
+
if (file.startsWith('.') && file !== '.github') continue;
|
|
160
|
+
if (file.includes('worktree')) continue;
|
|
138
161
|
walkDir(fullPath, fileList);
|
|
139
162
|
} else {
|
|
140
|
-
// Skip the scanner's own
|
|
163
|
+
// Skip the scanner's own CLI script so its rule definitions are not self-flagged
|
|
141
164
|
if (file === 'cli.js' && fullPath.includes('arnav-audit')) continue;
|
|
165
|
+
// Skip OS metadata
|
|
166
|
+
if (file === '.DS_Store' || file === 'Thumbs.db') continue;
|
|
142
167
|
|
|
143
|
-
const ext = path.extname(file);
|
|
144
|
-
if (
|
|
168
|
+
const ext = path.extname(file).toLowerCase();
|
|
169
|
+
if (!BINARY_EXTS.has(ext)) {
|
|
145
170
|
fileList.push(fullPath);
|
|
146
171
|
}
|
|
147
172
|
}
|
|
@@ -153,7 +178,7 @@ function walkDir(dir, fileList = []) {
|
|
|
153
178
|
|
|
154
179
|
// Pattern rules for security, leakage, and UI defects
|
|
155
180
|
const RULES = [
|
|
156
|
-
// 1. SECURITY & CREDENTIAL LEAKAGE
|
|
181
|
+
// 1. SECURITY & CREDENTIAL LEAKAGE (UNIVERSAL)
|
|
157
182
|
{
|
|
158
183
|
id: 'SEC-AWS-KEY',
|
|
159
184
|
category: 'Security',
|
|
@@ -196,15 +221,18 @@ const RULES = [
|
|
|
196
221
|
tier: 'CRITICAL',
|
|
197
222
|
title: 'Unencrypted Private Key Block in Code',
|
|
198
223
|
desc: 'RSA/EC/SSH private key found directly committed.',
|
|
199
|
-
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY
|
|
224
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\s*[\r\n]+[A-Za-z0-9+/=]{20,}/g,
|
|
200
225
|
remedy: 'Remove private key file from version control immediately; load via secure KMS/vault.'
|
|
201
226
|
},
|
|
227
|
+
|
|
228
|
+
// JAVASCRIPT & WEB CODE INJECTION
|
|
202
229
|
{
|
|
203
230
|
id: 'SEC-DANGEROUS-HTML',
|
|
204
231
|
category: 'Security',
|
|
205
232
|
tier: 'MAJOR',
|
|
206
233
|
title: 'Unsanitized dangerouslySetInnerHTML Injection',
|
|
207
234
|
desc: 'Direct usage of dangerouslySetInnerHTML without DOMPurify allows stored or reflected XSS.',
|
|
235
|
+
appliesTo: ['.js', '.jsx', '.ts', '.tsx', '.html'],
|
|
208
236
|
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!DOMPurify|sanitize)[a-zA-Z0-9_.]+/g,
|
|
209
237
|
remedy: 'Wrap raw HTML payload with DOMPurify.sanitize(dirtyHtml) before rendering.'
|
|
210
238
|
},
|
|
@@ -214,17 +242,41 @@ const RULES = [
|
|
|
214
242
|
tier: 'CRITICAL',
|
|
215
243
|
title: 'Dangerous eval() or new Function() Execution',
|
|
216
244
|
desc: 'Dynamic code execution opens remote arbitrary code execution vectors.',
|
|
217
|
-
|
|
245
|
+
appliesTo: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
|
|
246
|
+
regex: /(?<![.\w])eval\s*\(|\bnew\s+Function\s*\(/g,
|
|
218
247
|
remedy: 'Refactor dynamic evaluation to structured JSON.parse() or typed lookups.'
|
|
219
248
|
},
|
|
220
249
|
|
|
221
|
-
//
|
|
250
|
+
// PYTHON BACKEND SECURITY
|
|
251
|
+
{
|
|
252
|
+
id: 'SEC-PY-SQL-INJECTION',
|
|
253
|
+
category: 'Security',
|
|
254
|
+
tier: 'CRITICAL',
|
|
255
|
+
title: 'Python SQL Query String Formatting Injection',
|
|
256
|
+
desc: 'Raw f-string or % formatting directly inside SQL query execution.',
|
|
257
|
+
appliesTo: ['.py'],
|
|
258
|
+
regex: /cursor\.execute\(\s*f["']|execute\(\s*f["']SELECT/gi,
|
|
259
|
+
remedy: 'Use parameterized queries: execute("SELECT ... WHERE id = :id", {"id": val}).'
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'SEC-PY-SHELL-TRUE',
|
|
263
|
+
category: 'Security',
|
|
264
|
+
tier: 'CRITICAL',
|
|
265
|
+
title: 'Python Subprocess shell=True Command Injection',
|
|
266
|
+
desc: 'Running shell commands with shell=True allows arbitrary remote command execution.',
|
|
267
|
+
appliesTo: ['.py'],
|
|
268
|
+
regex: /subprocess\.(?:run|call|Popen)\([^)]*shell\s*=\s*True/g,
|
|
269
|
+
remedy: 'Pass command arguments as an array ["cmd", "arg1"] with shell=False.'
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// 2. MEMORY & RESOURCE LEAKAGE (FRONTEND JS/TS)
|
|
222
273
|
{
|
|
223
274
|
id: 'LEAK-EVENT-LISTENER',
|
|
224
275
|
category: 'Memory Leakage',
|
|
225
276
|
tier: 'MAJOR',
|
|
226
277
|
title: 'Dangling EventListener in Hook (Missing Clean-up)',
|
|
227
278
|
desc: 'addEventListener called inside useEffect without a corresponding removeEventListener in return cleanup.',
|
|
279
|
+
appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
|
|
228
280
|
customCheck: (content) => {
|
|
229
281
|
if (!content.includes('addEventListener')) return null;
|
|
230
282
|
if (content.includes('useEffect') && !content.includes('removeEventListener')) {
|
|
@@ -240,6 +292,7 @@ const RULES = [
|
|
|
240
292
|
tier: 'MAJOR',
|
|
241
293
|
title: 'Uncleaned setInterval / setTimeout Timer Leak',
|
|
242
294
|
desc: 'setInterval called in lifecycle without clearInterval unmount teardown, continuing to run in background.',
|
|
295
|
+
appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
|
|
243
296
|
customCheck: (content) => {
|
|
244
297
|
if (!content.includes('setInterval')) return null;
|
|
245
298
|
if (content.includes('useEffect') && !content.includes('clearInterval')) {
|
|
@@ -255,7 +308,8 @@ const RULES = [
|
|
|
255
308
|
tier: 'MINOR',
|
|
256
309
|
title: 'Global Window Scope Object Pollution',
|
|
257
310
|
desc: 'Assigning arbitrary variables to global window object prevents garbage collection.',
|
|
258
|
-
|
|
311
|
+
appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
|
|
312
|
+
regex: /window\.(?!__)[a-zA-Z0-9_$]+\s*=\s*(?!addEventListener|removeEventListener|location|scrollTo)[a-zA-Z0-9_$]+/g,
|
|
259
313
|
remedy: 'Encapsulate module state in React context, closures, or scoped module instances.'
|
|
260
314
|
},
|
|
261
315
|
{
|
|
@@ -264,6 +318,7 @@ const RULES = [
|
|
|
264
318
|
tier: 'SUGGESTION',
|
|
265
319
|
title: 'Residual Debug Console Logging',
|
|
266
320
|
desc: 'Found console.log statements that can leak internal state and data payloads to browser devtools.',
|
|
321
|
+
appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
|
|
267
322
|
regex: /console\.log\s*\(/g,
|
|
268
323
|
remedy: 'Remove console.log statements or strip via build compiler (e.g. babel-plugin-transform-remove-console).'
|
|
269
324
|
},
|
|
@@ -275,6 +330,7 @@ const RULES = [
|
|
|
275
330
|
tier: 'CRITICAL',
|
|
276
331
|
title: 'Mobile iOS Safari Input Auto-Zoom Trap',
|
|
277
332
|
desc: 'Text input font-size configured under 16px triggers mandatory Safari viewport zoom on focus.',
|
|
333
|
+
appliesTo: ['.css', '.scss', '.html', '.jsx', '.tsx'],
|
|
278
334
|
regex: /(?:input|textarea)[^{]*\{[^}]*font-size\s*:\s*(?:1[0-5]|[89])px/gi,
|
|
279
335
|
remedy: 'Enforce font-size: 16px minimum on mobile viewports for all form inputs (@media max-width: 768px).'
|
|
280
336
|
},
|
|
@@ -284,8 +340,9 @@ const RULES = [
|
|
|
284
340
|
tier: 'MAJOR',
|
|
285
341
|
title: '300ms Mobile Tap Delay Latency',
|
|
286
342
|
desc: 'Clickable elements missing touch-action: manipulation incur 300ms double-tap delay.',
|
|
287
|
-
|
|
288
|
-
|
|
343
|
+
appliesTo: ['.css', '.scss'],
|
|
344
|
+
customCheck: (content) => {
|
|
345
|
+
if (content.includes('cursor: pointer') && !content.includes('touch-action: manipulation')) {
|
|
289
346
|
return 'cursor: pointer declared on touch buttons without touch-action: manipulation.';
|
|
290
347
|
}
|
|
291
348
|
return null;
|
|
@@ -298,6 +355,7 @@ const RULES = [
|
|
|
298
355
|
tier: 'MAJOR',
|
|
299
356
|
title: 'Obliterated Keyboard Focus Ring',
|
|
300
357
|
desc: 'outline: none or outline: 0 destroys accessibility keyboard indicator (WCAG 2.4.7 violation).',
|
|
358
|
+
appliesTo: ['.css', '.scss', '.html'],
|
|
301
359
|
regex: /(?:button|a|\.btn)[^{]*\{[^}]*outline\s*:\s*(?:none|0)\b(?!.*focus-visible)/gi,
|
|
302
360
|
remedy: 'Replace outline: none with button:focus-visible { outline: 2px solid #00f5a0; outline-offset: 2px; }.'
|
|
303
361
|
},
|
|
@@ -307,6 +365,7 @@ const RULES = [
|
|
|
307
365
|
tier: 'MAJOR',
|
|
308
366
|
title: 'Flexbox SVG Icon Geometry Distortion',
|
|
309
367
|
desc: 'SVG icons placed directly inside flex containers collapse when sibling text wraps or grows.',
|
|
368
|
+
appliesTo: ['.jsx', '.tsx', '.html'],
|
|
310
369
|
customCheck: (content) => {
|
|
311
370
|
if (content.includes('display: flex') && content.includes('<svg') && !content.includes('flex-shrink: 0')) {
|
|
312
371
|
return 'Flex container contains SVG icons without flex-shrink: 0 declaration.';
|
|
@@ -321,7 +380,8 @@ const RULES = [
|
|
|
321
380
|
tier: 'MAJOR',
|
|
322
381
|
title: 'Horizontal Viewport Bleed (100vw Scrollbar Trap)',
|
|
323
382
|
desc: 'Using width: 100vw includes the scrollbar gutter width, triggering unwanted horizontal overflow.',
|
|
324
|
-
|
|
383
|
+
appliesTo: ['.css', '.scss', '.html', '.jsx', '.tsx'],
|
|
384
|
+
regex: /(?<!max-|min-)width\s*:\s*100vw/gi,
|
|
325
385
|
remedy: 'Replace width: 100vw with width: 100% or use max-w-full to prevent horizontal layout thrashing.'
|
|
326
386
|
},
|
|
327
387
|
{
|
|
@@ -330,6 +390,7 @@ const RULES = [
|
|
|
330
390
|
tier: 'MAJOR',
|
|
331
391
|
title: 'Unannounced Icon-Only Button',
|
|
332
392
|
desc: 'Buttons containing only an SVG or icon without text or aria-label are completely invisible to screen readers.',
|
|
393
|
+
appliesTo: ['.jsx', '.tsx', '.html'],
|
|
333
394
|
regex: /<button[^>]*>\s*<(?:svg|LucideIcon|[A-Z][a-zA-Z]+Icon)[^>]*\/>\s*<\/button>/g,
|
|
334
395
|
remedy: 'Add aria-label="Action Name" or title attribute to all icon-only buttons.'
|
|
335
396
|
}
|
|
@@ -344,10 +405,20 @@ function runLocalAudit(targetDir) {
|
|
|
344
405
|
|
|
345
406
|
const files = walkDir(root);
|
|
346
407
|
const issues = [];
|
|
408
|
+
const scannedFiles = [];
|
|
409
|
+
const extCounts = {};
|
|
410
|
+
const dirCounts = {};
|
|
347
411
|
|
|
348
412
|
for (const filePath of files) {
|
|
349
413
|
const relPath = path.relative(root, filePath);
|
|
350
|
-
const ext = path.extname(filePath);
|
|
414
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
415
|
+
scannedFiles.push(relPath);
|
|
416
|
+
|
|
417
|
+
const extKey = ext || '[config]';
|
|
418
|
+
extCounts[extKey] = (extCounts[extKey] || 0) + 1;
|
|
419
|
+
|
|
420
|
+
const topFolder = relPath.split(path.sep)[0] || '.';
|
|
421
|
+
dirCounts[topFolder] = (dirCounts[topFolder] || 0) + 1;
|
|
351
422
|
|
|
352
423
|
let content = '';
|
|
353
424
|
try {
|
|
@@ -356,24 +427,39 @@ function runLocalAudit(targetDir) {
|
|
|
356
427
|
continue;
|
|
357
428
|
}
|
|
358
429
|
|
|
359
|
-
// Check .env
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
430
|
+
// Check raw un-scoped .env files that are missing from .gitignore
|
|
431
|
+
if (path.basename(filePath) === '.env') {
|
|
432
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
433
|
+
let isGitIgnored = false;
|
|
434
|
+
if (fs.existsSync(gitignorePath)) {
|
|
435
|
+
try {
|
|
436
|
+
const giContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
437
|
+
if (giContent.includes('.env')) isGitIgnored = true;
|
|
438
|
+
} catch (e) {}
|
|
439
|
+
}
|
|
440
|
+
if (!isGitIgnored) {
|
|
441
|
+
issues.push({
|
|
442
|
+
id: 'SEC-ENV-EXPOSED',
|
|
443
|
+
category: 'Security',
|
|
444
|
+
tier: 'CRITICAL',
|
|
445
|
+
title: 'Environment Config File Committed to Source',
|
|
446
|
+
file: relPath,
|
|
447
|
+
line: 1,
|
|
448
|
+
snippet: 'Sensitive configuration file present in directory tree without .gitignore protection.',
|
|
449
|
+
remedy: 'Add .env* to .gitignore and remove from git index using git rm --cached.'
|
|
450
|
+
});
|
|
451
|
+
}
|
|
371
452
|
}
|
|
372
453
|
|
|
373
454
|
for (const rule of RULES) {
|
|
374
455
|
if (options.securityOnly && rule.category !== 'Security') continue;
|
|
375
456
|
if (options.leakageOnly && !rule.category.includes('Leakage')) continue;
|
|
376
457
|
|
|
458
|
+
// Filter by language/file extension if rule specifies appliesTo
|
|
459
|
+
if (rule.appliesTo && !rule.appliesTo.includes(ext)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
377
463
|
if (rule.regex) {
|
|
378
464
|
rule.regex.lastIndex = 0;
|
|
379
465
|
let match;
|
|
@@ -416,7 +502,7 @@ function runLocalAudit(targetDir) {
|
|
|
416
502
|
}
|
|
417
503
|
}
|
|
418
504
|
|
|
419
|
-
return { root, totalFiles: files.length, issues };
|
|
505
|
+
return { root, totalFiles: files.length, scannedFiles, extCounts, dirCounts, issues };
|
|
420
506
|
}
|
|
421
507
|
|
|
422
508
|
// -------------------------------------------------------------
|
|
@@ -518,6 +604,7 @@ async function main() {
|
|
|
518
604
|
console.log(JSON.stringify({
|
|
519
605
|
target: audit.root,
|
|
520
606
|
totalFiles: audit.totalFiles,
|
|
607
|
+
scannedFiles: audit.scannedFiles,
|
|
521
608
|
score,
|
|
522
609
|
grade,
|
|
523
610
|
counts: {
|
|
@@ -532,18 +619,54 @@ async function main() {
|
|
|
532
619
|
return;
|
|
533
620
|
}
|
|
534
621
|
|
|
622
|
+
// Print all scanned files if --files or --verbose requested
|
|
623
|
+
if (options.files || options.verbose) {
|
|
624
|
+
console.log(`${C.bold}VERIFIED CODEBASE FILES (${audit.totalFiles} files inspected):${C.reset}`);
|
|
625
|
+
audit.scannedFiles.forEach((file, idx) => {
|
|
626
|
+
const fileIssues = audit.issues.filter(i => i.file === file);
|
|
627
|
+
const numStr = String(idx + 1).padStart(3, ' ');
|
|
628
|
+
if (fileIssues.length === 0) {
|
|
629
|
+
console.log(` ${C.gray}[${numStr}/${audit.totalFiles}]${C.reset} ${C.emerald}✓${C.reset} ${file}`);
|
|
630
|
+
} else {
|
|
631
|
+
console.log(` ${C.gray}[${numStr}/${audit.totalFiles}]${C.reset} ${C.rose}✗${C.reset} ${file} ${C.rose}(${fileIssues.length} issues)${C.reset}`);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
console.log('');
|
|
635
|
+
}
|
|
636
|
+
|
|
535
637
|
// Formatted Terminal Dashboard
|
|
536
638
|
console.log(`${C.dim}————————————————————————————————————————————————————————————————————${C.reset}`);
|
|
537
|
-
console.log(` ${C.bold}AUDIT REPORT OVERVIEW${C.reset} • Scanned ${C.bold}${audit.totalFiles}${C.reset} files`);
|
|
639
|
+
console.log(` ${C.bold}AUDIT REPORT OVERVIEW${C.reset} • Scanned ${C.bold}${audit.totalFiles}${C.reset} files across entire codebase`);
|
|
538
640
|
console.log(`${C.dim}————————————————————————————————————————————————————————————————————${C.reset}`);
|
|
539
641
|
|
|
540
642
|
const gradeColor = grade.startsWith('A') ? C.emerald : grade === 'B' ? C.cyan : grade === 'C' ? C.amber : C.rose;
|
|
541
643
|
console.log(` Overall Score: ${gradeColor}${C.bold}${score}/100${C.reset} (Grade ${gradeColor}${C.bold}${grade}${C.reset})`);
|
|
542
644
|
console.log(` Findings: ${C.rose}${critical.length} Critical${C.reset} | ${C.amber}${major.length} Major${C.reset} | ${C.cyan}${minor.length} Minor${C.reset} | ${C.gray}${suggestions.length} Suggestions${C.reset}\n`);
|
|
543
645
|
|
|
646
|
+
console.log(` ${C.bold}Full Codebase Coverage Breakdown:${C.reset}`);
|
|
647
|
+
const sortedExts = Object.entries(audit.extCounts).sort((a, b) => b[1] - a[1]);
|
|
648
|
+
sortedExts.forEach(([ext, count]) => {
|
|
649
|
+
let name = 'Source / Script';
|
|
650
|
+
if (ext === '.py') name = 'Python (FastAPI, Worker, Checks, Tests)';
|
|
651
|
+
else if (ext === '.ts') name = 'TypeScript (Backend, Core, API routes)';
|
|
652
|
+
else if (ext === '.tsx') name = 'React / Next.js Components';
|
|
653
|
+
else if (ext === '.js') name = 'JavaScript Modules & Configs';
|
|
654
|
+
else if (ext === '.json') name = 'JSON Manifests & Data schemas';
|
|
655
|
+
else if (ext === '.yml' || ext === '.yaml') name = 'GitHub CI & Docker Compose';
|
|
656
|
+
else if (ext === '.css' || ext === '.scss') name = 'Vanilla & Tailwind Styles';
|
|
657
|
+
else if (ext === '.html') name = 'HTML Templates & Fixtures';
|
|
658
|
+
else if (ext === '.md') name = 'Documentation & Blueprints';
|
|
659
|
+
else if (ext === '[config]') name = 'Dockerfiles, .env, .gitignore';
|
|
660
|
+
console.log(` • ${C.cyan}${name.padEnd(42, ' ')}${C.reset} ${C.bold}${count}${C.reset} files (${ext})`);
|
|
661
|
+
});
|
|
662
|
+
console.log('');
|
|
663
|
+
|
|
544
664
|
if (audit.issues.length === 0) {
|
|
545
|
-
console.log(` ${C.emerald}✓ Zero internal security, leakage, or interface defects detected!${C.reset}
|
|
546
|
-
console.log(`
|
|
665
|
+
console.log(` ${C.emerald}✓ Zero internal security, leakage, or interface defects detected!${C.reset}`);
|
|
666
|
+
console.log(` Every file in the codebase was verified. Meets enterprise standards.\n`);
|
|
667
|
+
if (!options.files) {
|
|
668
|
+
console.log(` ${C.dim}Tip: Run with ${C.white}--files${C.dim} to display each individual file path in the terminal.${C.reset}`);
|
|
669
|
+
}
|
|
547
670
|
console.log(`${C.dim}————————————————————————————————————————————————————————————————————${C.reset}\n`);
|
|
548
671
|
return;
|
|
549
672
|
}
|