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.
Files changed (2) hide show
  1. package/bin/cli.js +155 -32
  2. 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.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 ALLOWED_EXTS = new Set([
125
- '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
126
- '.css', '.scss', '.html', '.json', '.env', '.yaml', '.yml'
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 implementation file so its rule regexes are not self-flagged
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 (ALLOWED_EXTS.has(ext) || file.startsWith('.env')) {
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-----/g,
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
- regex: /\beval\s*\(|\bnew\s+Function\s*\(/g,
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
- // 2. MEMORY & RESOURCE LEAKAGE
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
- regex: /window\.[a-zA-Z0-9_$]+\s*=\s*(?!addEventListener|removeEventListener|location|scrollTo)[a-zA-Z0-9_$]+/g,
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
- customCheck: (content, ext) => {
288
- if (ext === '.css' && content.includes('cursor: pointer') && !content.includes('touch-action: manipulation')) {
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
- regex: /width\s*:\s*100vw/gi,
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 in git
360
- if (filePath.endsWith('.env') || filePath.endsWith('.env.local')) {
361
- issues.push({
362
- id: 'SEC-ENV-EXPOSED',
363
- category: 'Security',
364
- tier: 'CRITICAL',
365
- title: 'Environment Config File Committed to Source',
366
- file: relPath,
367
- line: 1,
368
- snippet: 'Sensitive configuration file present in directory tree.',
369
- remedy: 'Add .env* to .gitignore and remove from git index using git rm --cached.'
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}\n`);
546
- console.log(` Your codebase meets enterprise production standards.`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arnav-audit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Comprehensive internal security, memory leakage, and invisible UI/UX interface auditor by Arnav.",
5
5
  "bin": {
6
6
  "arnav-audit": "bin/cli.js"