arnav-audit 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/cli.js +97 -28
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -73,7 +73,7 @@ for (let i = 0; i < args.length; i++) {
73
73
  }
74
74
 
75
75
  if (options.version) {
76
- console.log('arnav-audit v1.0.0');
76
+ console.log('arnav-audit v1.0.1');
77
77
  process.exit(0);
78
78
  }
79
79
 
@@ -112,18 +112,31 @@ const IGNORED_DIRS = new Set([
112
112
  '.git',
113
113
  '.next',
114
114
  '.vercel',
115
+ '.kilo',
116
+ '.pytest_cache',
117
+ 'artifacts',
118
+ 'test-results',
119
+ 'playwright-report',
115
120
  'dist',
116
121
  'build',
117
122
  'coverage',
118
123
  '.cache',
119
124
  'venv',
120
125
  '.venv',
126
+ 'env',
121
127
  '__pycache__',
122
128
  ]);
123
129
 
124
- const ALLOWED_EXTS = new Set([
125
- '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
126
- '.css', '.scss', '.html', '.json', '.env', '.yaml', '.yml'
130
+ const BINARY_EXTS = new Set([
131
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.avif', '.svgz',
132
+ '.mp4', '.webm', '.ogg', '.mp3', '.wav', '.flac',
133
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
134
+ '.zip', '.tar', '.gz', '.tgz', '.rar', '.7z',
135
+ '.exe', '.dll', '.so', '.dylib', '.bin',
136
+ '.pyc', '.pyo', '.pyd',
137
+ '.db', '.sqlite', '.sqlite3',
138
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
139
+ '.tsbuildinfo'
127
140
  ]);
128
141
 
129
142
  function walkDir(dir, fileList = []) {
@@ -135,13 +148,18 @@ function walkDir(dir, fileList = []) {
135
148
  try {
136
149
  const stat = fs.statSync(fullPath);
137
150
  if (stat.isDirectory()) {
151
+ // Allow .github workflow directory while ignoring other hidden dot-folders
152
+ if (file.startsWith('.') && file !== '.github') continue;
153
+ if (file.includes('worktree')) continue;
138
154
  walkDir(fullPath, fileList);
139
155
  } else {
140
- // Skip the scanner's own implementation file so its rule regexes are not self-flagged
156
+ // Skip the scanner's own CLI script so its rule definitions are not self-flagged
141
157
  if (file === 'cli.js' && fullPath.includes('arnav-audit')) continue;
158
+ // Skip OS metadata
159
+ if (file === '.DS_Store' || file === 'Thumbs.db') continue;
142
160
 
143
- const ext = path.extname(file);
144
- if (ALLOWED_EXTS.has(ext) || file.startsWith('.env')) {
161
+ const ext = path.extname(file).toLowerCase();
162
+ if (!BINARY_EXTS.has(ext)) {
145
163
  fileList.push(fullPath);
146
164
  }
147
165
  }
@@ -153,7 +171,7 @@ function walkDir(dir, fileList = []) {
153
171
 
154
172
  // Pattern rules for security, leakage, and UI defects
155
173
  const RULES = [
156
- // 1. SECURITY & CREDENTIAL LEAKAGE
174
+ // 1. SECURITY & CREDENTIAL LEAKAGE (UNIVERSAL)
157
175
  {
158
176
  id: 'SEC-AWS-KEY',
159
177
  category: 'Security',
@@ -196,15 +214,18 @@ const RULES = [
196
214
  tier: 'CRITICAL',
197
215
  title: 'Unencrypted Private Key Block in Code',
198
216
  desc: 'RSA/EC/SSH private key found directly committed.',
199
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
217
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\s*[\r\n]+[A-Za-z0-9+/=]{20,}/g,
200
218
  remedy: 'Remove private key file from version control immediately; load via secure KMS/vault.'
201
219
  },
220
+
221
+ // JAVASCRIPT & WEB CODE INJECTION
202
222
  {
203
223
  id: 'SEC-DANGEROUS-HTML',
204
224
  category: 'Security',
205
225
  tier: 'MAJOR',
206
226
  title: 'Unsanitized dangerouslySetInnerHTML Injection',
207
227
  desc: 'Direct usage of dangerouslySetInnerHTML without DOMPurify allows stored or reflected XSS.',
228
+ appliesTo: ['.js', '.jsx', '.ts', '.tsx', '.html'],
208
229
  regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!DOMPurify|sanitize)[a-zA-Z0-9_.]+/g,
209
230
  remedy: 'Wrap raw HTML payload with DOMPurify.sanitize(dirtyHtml) before rendering.'
210
231
  },
@@ -214,17 +235,41 @@ const RULES = [
214
235
  tier: 'CRITICAL',
215
236
  title: 'Dangerous eval() or new Function() Execution',
216
237
  desc: 'Dynamic code execution opens remote arbitrary code execution vectors.',
217
- regex: /\beval\s*\(|\bnew\s+Function\s*\(/g,
238
+ appliesTo: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
239
+ regex: /(?<![.\w])eval\s*\(|\bnew\s+Function\s*\(/g,
218
240
  remedy: 'Refactor dynamic evaluation to structured JSON.parse() or typed lookups.'
219
241
  },
220
242
 
221
- // 2. MEMORY & RESOURCE LEAKAGE
243
+ // PYTHON BACKEND SECURITY
244
+ {
245
+ id: 'SEC-PY-SQL-INJECTION',
246
+ category: 'Security',
247
+ tier: 'CRITICAL',
248
+ title: 'Python SQL Query String Formatting Injection',
249
+ desc: 'Raw f-string or % formatting directly inside SQL query execution.',
250
+ appliesTo: ['.py'],
251
+ regex: /cursor\.execute\(\s*f["']|execute\(\s*f["']SELECT/gi,
252
+ remedy: 'Use parameterized queries: execute("SELECT ... WHERE id = :id", {"id": val}).'
253
+ },
254
+ {
255
+ id: 'SEC-PY-SHELL-TRUE',
256
+ category: 'Security',
257
+ tier: 'CRITICAL',
258
+ title: 'Python Subprocess shell=True Command Injection',
259
+ desc: 'Running shell commands with shell=True allows arbitrary remote command execution.',
260
+ appliesTo: ['.py'],
261
+ regex: /subprocess\.(?:run|call|Popen)\([^)]*shell\s*=\s*True/g,
262
+ remedy: 'Pass command arguments as an array ["cmd", "arg1"] with shell=False.'
263
+ },
264
+
265
+ // 2. MEMORY & RESOURCE LEAKAGE (FRONTEND JS/TS)
222
266
  {
223
267
  id: 'LEAK-EVENT-LISTENER',
224
268
  category: 'Memory Leakage',
225
269
  tier: 'MAJOR',
226
270
  title: 'Dangling EventListener in Hook (Missing Clean-up)',
227
271
  desc: 'addEventListener called inside useEffect without a corresponding removeEventListener in return cleanup.',
272
+ appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
228
273
  customCheck: (content) => {
229
274
  if (!content.includes('addEventListener')) return null;
230
275
  if (content.includes('useEffect') && !content.includes('removeEventListener')) {
@@ -240,6 +285,7 @@ const RULES = [
240
285
  tier: 'MAJOR',
241
286
  title: 'Uncleaned setInterval / setTimeout Timer Leak',
242
287
  desc: 'setInterval called in lifecycle without clearInterval unmount teardown, continuing to run in background.',
288
+ appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
243
289
  customCheck: (content) => {
244
290
  if (!content.includes('setInterval')) return null;
245
291
  if (content.includes('useEffect') && !content.includes('clearInterval')) {
@@ -255,7 +301,8 @@ const RULES = [
255
301
  tier: 'MINOR',
256
302
  title: 'Global Window Scope Object Pollution',
257
303
  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,
304
+ appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
305
+ regex: /window\.(?!__)[a-zA-Z0-9_$]+\s*=\s*(?!addEventListener|removeEventListener|location|scrollTo)[a-zA-Z0-9_$]+/g,
259
306
  remedy: 'Encapsulate module state in React context, closures, or scoped module instances.'
260
307
  },
261
308
  {
@@ -264,6 +311,7 @@ const RULES = [
264
311
  tier: 'SUGGESTION',
265
312
  title: 'Residual Debug Console Logging',
266
313
  desc: 'Found console.log statements that can leak internal state and data payloads to browser devtools.',
314
+ appliesTo: ['.js', '.jsx', '.ts', '.tsx'],
267
315
  regex: /console\.log\s*\(/g,
268
316
  remedy: 'Remove console.log statements or strip via build compiler (e.g. babel-plugin-transform-remove-console).'
269
317
  },
@@ -275,6 +323,7 @@ const RULES = [
275
323
  tier: 'CRITICAL',
276
324
  title: 'Mobile iOS Safari Input Auto-Zoom Trap',
277
325
  desc: 'Text input font-size configured under 16px triggers mandatory Safari viewport zoom on focus.',
326
+ appliesTo: ['.css', '.scss', '.html', '.jsx', '.tsx'],
278
327
  regex: /(?:input|textarea)[^{]*\{[^}]*font-size\s*:\s*(?:1[0-5]|[89])px/gi,
279
328
  remedy: 'Enforce font-size: 16px minimum on mobile viewports for all form inputs (@media max-width: 768px).'
280
329
  },
@@ -284,8 +333,9 @@ const RULES = [
284
333
  tier: 'MAJOR',
285
334
  title: '300ms Mobile Tap Delay Latency',
286
335
  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')) {
336
+ appliesTo: ['.css', '.scss'],
337
+ customCheck: (content) => {
338
+ if (content.includes('cursor: pointer') && !content.includes('touch-action: manipulation')) {
289
339
  return 'cursor: pointer declared on touch buttons without touch-action: manipulation.';
290
340
  }
291
341
  return null;
@@ -298,6 +348,7 @@ const RULES = [
298
348
  tier: 'MAJOR',
299
349
  title: 'Obliterated Keyboard Focus Ring',
300
350
  desc: 'outline: none or outline: 0 destroys accessibility keyboard indicator (WCAG 2.4.7 violation).',
351
+ appliesTo: ['.css', '.scss', '.html'],
301
352
  regex: /(?:button|a|\.btn)[^{]*\{[^}]*outline\s*:\s*(?:none|0)\b(?!.*focus-visible)/gi,
302
353
  remedy: 'Replace outline: none with button:focus-visible { outline: 2px solid #00f5a0; outline-offset: 2px; }.'
303
354
  },
@@ -307,6 +358,7 @@ const RULES = [
307
358
  tier: 'MAJOR',
308
359
  title: 'Flexbox SVG Icon Geometry Distortion',
309
360
  desc: 'SVG icons placed directly inside flex containers collapse when sibling text wraps or grows.',
361
+ appliesTo: ['.jsx', '.tsx', '.html'],
310
362
  customCheck: (content) => {
311
363
  if (content.includes('display: flex') && content.includes('<svg') && !content.includes('flex-shrink: 0')) {
312
364
  return 'Flex container contains SVG icons without flex-shrink: 0 declaration.';
@@ -321,7 +373,8 @@ const RULES = [
321
373
  tier: 'MAJOR',
322
374
  title: 'Horizontal Viewport Bleed (100vw Scrollbar Trap)',
323
375
  desc: 'Using width: 100vw includes the scrollbar gutter width, triggering unwanted horizontal overflow.',
324
- regex: /width\s*:\s*100vw/gi,
376
+ appliesTo: ['.css', '.scss', '.html', '.jsx', '.tsx'],
377
+ regex: /(?<!max-|min-)width\s*:\s*100vw/gi,
325
378
  remedy: 'Replace width: 100vw with width: 100% or use max-w-full to prevent horizontal layout thrashing.'
326
379
  },
327
380
  {
@@ -330,6 +383,7 @@ const RULES = [
330
383
  tier: 'MAJOR',
331
384
  title: 'Unannounced Icon-Only Button',
332
385
  desc: 'Buttons containing only an SVG or icon without text or aria-label are completely invisible to screen readers.',
386
+ appliesTo: ['.jsx', '.tsx', '.html'],
333
387
  regex: /<button[^>]*>\s*<(?:svg|LucideIcon|[A-Z][a-zA-Z]+Icon)[^>]*\/>\s*<\/button>/g,
334
388
  remedy: 'Add aria-label="Action Name" or title attribute to all icon-only buttons.'
335
389
  }
@@ -347,7 +401,7 @@ function runLocalAudit(targetDir) {
347
401
 
348
402
  for (const filePath of files) {
349
403
  const relPath = path.relative(root, filePath);
350
- const ext = path.extname(filePath);
404
+ const ext = path.extname(filePath).toLowerCase();
351
405
 
352
406
  let content = '';
353
407
  try {
@@ -356,24 +410,39 @@ function runLocalAudit(targetDir) {
356
410
  continue;
357
411
  }
358
412
 
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
- });
413
+ // Check raw un-scoped .env files that are missing from .gitignore
414
+ if (path.basename(filePath) === '.env') {
415
+ const gitignorePath = path.join(root, '.gitignore');
416
+ let isGitIgnored = false;
417
+ if (fs.existsSync(gitignorePath)) {
418
+ try {
419
+ const giContent = fs.readFileSync(gitignorePath, 'utf8');
420
+ if (giContent.includes('.env')) isGitIgnored = true;
421
+ } catch (e) {}
422
+ }
423
+ if (!isGitIgnored) {
424
+ issues.push({
425
+ id: 'SEC-ENV-EXPOSED',
426
+ category: 'Security',
427
+ tier: 'CRITICAL',
428
+ title: 'Environment Config File Committed to Source',
429
+ file: relPath,
430
+ line: 1,
431
+ snippet: 'Sensitive configuration file present in directory tree without .gitignore protection.',
432
+ remedy: 'Add .env* to .gitignore and remove from git index using git rm --cached.'
433
+ });
434
+ }
371
435
  }
372
436
 
373
437
  for (const rule of RULES) {
374
438
  if (options.securityOnly && rule.category !== 'Security') continue;
375
439
  if (options.leakageOnly && !rule.category.includes('Leakage')) continue;
376
440
 
441
+ // Filter by language/file extension if rule specifies appliesTo
442
+ if (rule.appliesTo && !rule.appliesTo.includes(ext)) {
443
+ continue;
444
+ }
445
+
377
446
  if (rule.regex) {
378
447
  rule.regex.lastIndex = 0;
379
448
  let match;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arnav-audit",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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"