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.
- package/bin/cli.js +97 -28
- 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.
|
|
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
|
|
125
|
-
'.
|
|
126
|
-
'.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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;
|