xploitscan 0.4.0 → 0.5.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/index.js +275 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -335,15 +335,21 @@ var hardcodedSecrets = {
|
|
|
335
335
|
];
|
|
336
336
|
const matches = [];
|
|
337
337
|
for (const pattern of patterns) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
() => "Move this secret to an environment variable and add it to .env (not committed to git). Use .env.example to document the required variables."
|
|
345
|
-
)
|
|
338
|
+
const rawMatches = findMatches(
|
|
339
|
+
content,
|
|
340
|
+
pattern,
|
|
341
|
+
hardcodedSecrets,
|
|
342
|
+
filePath,
|
|
343
|
+
() => "Move this secret to an environment variable and add it to .env (not committed to git). Use .env.example to document the required variables."
|
|
346
344
|
);
|
|
345
|
+
for (const rm3 of rawMatches) {
|
|
346
|
+
const lineText = content.split("\n")[rm3.line - 1] || "";
|
|
347
|
+
const trimmed = lineText.trimStart();
|
|
348
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#")) continue;
|
|
349
|
+
const secretMatch = lineText.match(/[:=]\s*["'`]([^"'`]*)["'`]/);
|
|
350
|
+
if (secretMatch && secretMatch[1].length < 12) continue;
|
|
351
|
+
matches.push(rm3);
|
|
352
|
+
}
|
|
347
353
|
}
|
|
348
354
|
return matches;
|
|
349
355
|
}
|
|
@@ -524,6 +530,7 @@ var xssVulnerability = {
|
|
|
524
530
|
check(content, filePath) {
|
|
525
531
|
if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
|
|
526
532
|
if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
533
|
+
if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
|
|
527
534
|
const patterns = [
|
|
528
535
|
// React dangerouslySetInnerHTML
|
|
529
536
|
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
@@ -765,6 +772,7 @@ var evalUsage = {
|
|
|
765
772
|
const lineEnd = content.indexOf("\n", lineStart + 1);
|
|
766
773
|
const lineText = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
|
|
767
774
|
if (/devtool|source-map/i.test(lineText)) continue;
|
|
775
|
+
if (/(?:description|title|message)\s*[:=]/i.test(lineText)) continue;
|
|
768
776
|
matches.push(rm3);
|
|
769
777
|
}
|
|
770
778
|
}
|
|
@@ -963,9 +971,9 @@ var unsanitizedHTMLExport = {
|
|
|
963
971
|
description: "Generating HTML from user content without sanitization (e.g., DOMPurify) allows stored XSS attacks. Malicious content saved in documents could execute scripts when exported or previewed.",
|
|
964
972
|
check(content, filePath) {
|
|
965
973
|
if (isTestFile(filePath)) return [];
|
|
966
|
-
if (/
|
|
974
|
+
if (/DOMPurify|dompurify/i.test(content)) return [];
|
|
967
975
|
if (filePath.match(/\.(jsx|tsx|vue|svelte)$/) && !/innerHTML|document\.write|\.html\s*=/i.test(content)) return [];
|
|
968
|
-
const hasSanitizer = /
|
|
976
|
+
const hasSanitizer = /sanitize|escapeHtml|escapeHTML|xss|htmlEncode|purify/i.test(content);
|
|
969
977
|
if (hasSanitizer) return [];
|
|
970
978
|
if (!/(?:export|download|save|write|send).*(?:html|HTML)|\.innerHTML\s*=|document\.write|res\.send\s*\(/i.test(content)) return [];
|
|
971
979
|
const matches = [];
|
|
@@ -1336,13 +1344,19 @@ var insecureRandomness = {
|
|
|
1336
1344
|
if (!/Math\.random\s*\(\s*\)/i.test(content)) return [];
|
|
1337
1345
|
const matches = [];
|
|
1338
1346
|
const securityContext = /(?:token|secret|session|password|otp|nonce|salt|csrf|auth)\s*[:=]\s*.*Math\.random/gi;
|
|
1339
|
-
|
|
1347
|
+
const rawMatches = findMatches(
|
|
1340
1348
|
content,
|
|
1341
1349
|
securityContext,
|
|
1342
1350
|
insecureRandomness,
|
|
1343
1351
|
filePath,
|
|
1344
1352
|
() => "Use crypto.randomUUID() or crypto.getRandomValues() for security-sensitive values. Math.random() is predictable."
|
|
1345
|
-
)
|
|
1353
|
+
);
|
|
1354
|
+
const nonSecurityVarNames = /(?:id|key|color|index|delay|position|size|width|height|offset|opacity|rotation|animation|random(?!.*(?:token|secret|key|password)))\s*[:=]\s*.*Math\.random/i;
|
|
1355
|
+
for (const rm3 of rawMatches) {
|
|
1356
|
+
const lineText = content.split("\n")[rm3.line - 1] || "";
|
|
1357
|
+
if (nonSecurityVarNames.test(lineText)) continue;
|
|
1358
|
+
matches.push(rm3);
|
|
1359
|
+
}
|
|
1346
1360
|
return matches;
|
|
1347
1361
|
}
|
|
1348
1362
|
};
|
|
@@ -1380,14 +1394,17 @@ var missingErrorBoundary = {
|
|
|
1380
1394
|
category: "Configuration",
|
|
1381
1395
|
description: "React apps without error boundaries display raw stack traces and component tree info to users when crashes occur, leaking internal details.",
|
|
1382
1396
|
check(content, filePath) {
|
|
1383
|
-
|
|
1397
|
+
const basename = filePath.split("/").pop() || "";
|
|
1398
|
+
if (!/^[Aa]pp\.[jt]sx$/i.test(basename)) return [];
|
|
1399
|
+
if (filePath.match(/\.ts$/)) return [];
|
|
1400
|
+
if (!/<[A-Z]/.test(content)) return [];
|
|
1384
1401
|
if (/(?:BrowserWindow|electron|ipcMain|app\.on\s*\(\s*["']ready)/i.test(content)) return [];
|
|
1385
|
-
if (
|
|
1402
|
+
if (/\/main\//.test(filePath) && !/react-dom|createRoot/i.test(content)) return [];
|
|
1386
1403
|
if (!/(?:import.*react|from\s+['"]react|require.*react)/i.test(content)) return [];
|
|
1387
|
-
if (!/(?:
|
|
1404
|
+
if (!/(?:createRoot|ReactDOM\.render)/i.test(content)) return [];
|
|
1388
1405
|
const hasErrorBoundary = /ErrorBoundary|componentDidCatch|getDerivedStateFromError|error-boundary/i.test(content);
|
|
1389
1406
|
if (hasErrorBoundary) return [];
|
|
1390
|
-
if (/createRoot|ReactDOM\.render
|
|
1407
|
+
if (/createRoot|ReactDOM\.render/i.test(content)) {
|
|
1391
1408
|
return [{
|
|
1392
1409
|
rule: "VC036",
|
|
1393
1410
|
title: missingErrorBoundary.title,
|
|
@@ -1517,13 +1534,23 @@ var ssrfVulnerability = {
|
|
|
1517
1534
|
const hasValidation = /allowedHosts|allowedDomains|allowedUrls|safeDomain|whitelist|urlValidator|new URL.*hostname.*includes|isAllowedUrl|validateUrl|isValidUrl/i.test(content);
|
|
1518
1535
|
if (hasValidation) return [];
|
|
1519
1536
|
for (const p of patterns) {
|
|
1520
|
-
|
|
1537
|
+
const rawMatches = findMatches(
|
|
1521
1538
|
content,
|
|
1522
1539
|
p,
|
|
1523
1540
|
ssrfVulnerability,
|
|
1524
1541
|
filePath,
|
|
1525
1542
|
() => "Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');"
|
|
1526
|
-
)
|
|
1543
|
+
);
|
|
1544
|
+
for (const rm3 of rawMatches) {
|
|
1545
|
+
const lineText = content.split("\n")[rm3.line - 1] || "";
|
|
1546
|
+
const varMatch = lineText.match(/(?:fetch|axios\.\w+|got|request|https?\.get)\s*\(\s*(\w+)/);
|
|
1547
|
+
if (varMatch) {
|
|
1548
|
+
const varName = varMatch[1];
|
|
1549
|
+
const constDef = new RegExp(`const\\s+${varName}\\s*=\\s*["'\`]https?://`, "i");
|
|
1550
|
+
if (constDef.test(content)) continue;
|
|
1551
|
+
}
|
|
1552
|
+
matches.push(rm3);
|
|
1553
|
+
}
|
|
1527
1554
|
}
|
|
1528
1555
|
return matches;
|
|
1529
1556
|
}
|
|
@@ -1630,13 +1657,21 @@ var weakPasswordRequirements = {
|
|
|
1630
1657
|
if (hasValidation) return [];
|
|
1631
1658
|
const hasPasswordHandling = /(?:password|pwd)\s*[:=]\s*(?:req\.body|body|input|params|args)\./i.test(content);
|
|
1632
1659
|
if (!hasPasswordHandling) return [];
|
|
1633
|
-
|
|
1660
|
+
const rawMatches = findMatches(
|
|
1634
1661
|
content,
|
|
1635
1662
|
/(?:password|pwd)\s*[:=]\s*(?:req\.body|body|input|params|args)\./gi,
|
|
1636
1663
|
weakPasswordRequirements,
|
|
1637
1664
|
filePath,
|
|
1638
1665
|
() => "Enforce minimum password requirements: at least 8 characters, mix of letters/numbers/symbols. Use a library like zxcvbn for strength estimation."
|
|
1639
1666
|
);
|
|
1667
|
+
const lines = content.split("\n");
|
|
1668
|
+
const validationPattern = /\.length|minLength|minlength|min_length|match|test|regex|pattern|validate|schema|zxcvbn|isStrongPassword/i;
|
|
1669
|
+
return rawMatches.filter((rm3) => {
|
|
1670
|
+
const start = Math.max(0, rm3.line - 1 - 10);
|
|
1671
|
+
const end = Math.min(lines.length, rm3.line - 1 + 10);
|
|
1672
|
+
const nearby = lines.slice(start, end).join("\n");
|
|
1673
|
+
return !validationPattern.test(nearby);
|
|
1674
|
+
});
|
|
1640
1675
|
}
|
|
1641
1676
|
};
|
|
1642
1677
|
var sessionFixation = {
|
|
@@ -1867,6 +1902,7 @@ var sensitiveLocalStorage = {
|
|
|
1867
1902
|
check(content, filePath) {
|
|
1868
1903
|
if (!filePath.match(/\.(jsx?|tsx?|vue|svelte)$/)) return [];
|
|
1869
1904
|
if (isTestFile(filePath)) return [];
|
|
1905
|
+
if (/mock|spec/i.test(filePath)) return [];
|
|
1870
1906
|
if (!/localStorage\.setItem|localStorage\[/i.test(content)) return [];
|
|
1871
1907
|
const matches = [];
|
|
1872
1908
|
const patterns = [
|
|
@@ -3132,6 +3168,203 @@ var complianceMap = {
|
|
|
3132
3168
|
VC095: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
3133
3169
|
VC096: { owasp: "A02:2021", cwe: "CWE-319" }
|
|
3134
3170
|
};
|
|
3171
|
+
var consoleLogProduction = {
|
|
3172
|
+
id: "VC097",
|
|
3173
|
+
title: "Console.log Left in Production Code",
|
|
3174
|
+
severity: "medium",
|
|
3175
|
+
category: "Performance",
|
|
3176
|
+
description: "console.log statements left in production code can leak sensitive data, slow down rendering, and clutter browser consoles.",
|
|
3177
|
+
check(content, filePath) {
|
|
3178
|
+
if (filePath.match(/test|spec|mock|__tests__|fixture|\.test\.|\.spec\./i)) return [];
|
|
3179
|
+
if (!/console\.log\s*\(/g.test(content)) return [];
|
|
3180
|
+
const lines = content.split("\n");
|
|
3181
|
+
const matches = [];
|
|
3182
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3183
|
+
const line = lines[i].trim();
|
|
3184
|
+
if (/console\.log\s*\(/.test(line) && !line.startsWith("//") && !line.startsWith("*") && !/if\s*\(\s*(?:debug|process\.env)/i.test(lines[Math.max(0, i - 1)] + line)) {
|
|
3185
|
+
matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "medium", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a logger that can be disabled in production." });
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
return matches.slice(0, 3);
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
var syncFileOps = {
|
|
3192
|
+
id: "VC098",
|
|
3193
|
+
title: "Synchronous File Operations",
|
|
3194
|
+
severity: "medium",
|
|
3195
|
+
category: "Performance",
|
|
3196
|
+
description: "Synchronous file operations (readFileSync, writeFileSync) block the event loop, causing all other requests to wait.",
|
|
3197
|
+
check(content, filePath) {
|
|
3198
|
+
if (filePath.match(/test|spec|mock|__tests__|fixture|config|\.config\./i)) return [];
|
|
3199
|
+
return findMatches(
|
|
3200
|
+
content,
|
|
3201
|
+
/(?:readFileSync|writeFileSync|appendFileSync|mkdirSync|rmdirSync|statSync)\s*\(/g,
|
|
3202
|
+
syncFileOps,
|
|
3203
|
+
filePath,
|
|
3204
|
+
() => "Use async file operations (readFile, writeFile) to avoid blocking the event loop."
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
3208
|
+
var eventListenerLeak = {
|
|
3209
|
+
id: "VC099",
|
|
3210
|
+
title: "Memory Leak: Event Listener Not Cleaned Up",
|
|
3211
|
+
severity: "high",
|
|
3212
|
+
category: "Performance",
|
|
3213
|
+
description: "Adding event listeners in React useEffect without a cleanup function causes memory leaks as listeners accumulate on re-renders.",
|
|
3214
|
+
check(content, filePath) {
|
|
3215
|
+
if (!filePath.match(/\.(jsx|tsx)$/)) return [];
|
|
3216
|
+
if (!/addEventListener/i.test(content)) return [];
|
|
3217
|
+
if (/removeEventListener/i.test(content)) return [];
|
|
3218
|
+
if (!/useEffect/i.test(content)) return [];
|
|
3219
|
+
return findMatches(
|
|
3220
|
+
content,
|
|
3221
|
+
/addEventListener\s*\(/g,
|
|
3222
|
+
eventListenerLeak,
|
|
3223
|
+
filePath,
|
|
3224
|
+
() => "Return a cleanup function from useEffect: useEffect(() => { window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []);"
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
var nPlusOneQuery = {
|
|
3229
|
+
id: "VC100",
|
|
3230
|
+
title: "N+1 Query Pattern Detected",
|
|
3231
|
+
severity: "medium",
|
|
3232
|
+
category: "Performance",
|
|
3233
|
+
description: "Database queries inside loops cause N+1 performance problems \u2014 one query per iteration instead of a single batch query.",
|
|
3234
|
+
check(content, filePath) {
|
|
3235
|
+
if (filePath.match(/test|spec|mock/i)) return [];
|
|
3236
|
+
const hasLoopWithQuery = /(?:for\s*\(|\.forEach\s*\(|\.map\s*\(|while\s*\()[^}]*(?:\.find\(|\.findOne\(|\.findById\(|\.query\(|\.execute\(|SELECT\s)/is.test(content);
|
|
3237
|
+
if (!hasLoopWithQuery) return [];
|
|
3238
|
+
return findMatches(
|
|
3239
|
+
content,
|
|
3240
|
+
/(?:for\s*\(|\.forEach\s*\(|\.map\s*\(|while\s*\()/g,
|
|
3241
|
+
nPlusOneQuery,
|
|
3242
|
+
filePath,
|
|
3243
|
+
() => "Fetch all data in a single query using WHERE IN, JOIN, or batch operations instead of querying per item in a loop."
|
|
3244
|
+
).slice(0, 2);
|
|
3245
|
+
}
|
|
3246
|
+
};
|
|
3247
|
+
var largeBundleImport = {
|
|
3248
|
+
id: "VC101",
|
|
3249
|
+
title: "Importing Entire Library (Large Bundle)",
|
|
3250
|
+
severity: "medium",
|
|
3251
|
+
category: "Performance",
|
|
3252
|
+
description: "Importing entire libraries like lodash or moment.js adds hundreds of KB to your bundle. Import only the functions you need.",
|
|
3253
|
+
check(content, filePath) {
|
|
3254
|
+
if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
|
|
3255
|
+
const matches = [];
|
|
3256
|
+
const patterns = [
|
|
3257
|
+
/import\s+_\s+from\s+['"]lodash['"]/g,
|
|
3258
|
+
/import\s+\*\s+as\s+_\s+from\s+['"]lodash['"]/g,
|
|
3259
|
+
/import\s+moment\s+from\s+['"]moment['"]/g,
|
|
3260
|
+
/const\s+_\s*=\s*require\s*\(\s*['"]lodash['"]\s*\)/g,
|
|
3261
|
+
/const\s+moment\s*=\s*require\s*\(\s*['"]moment['"]\s*\)/g
|
|
3262
|
+
];
|
|
3263
|
+
for (const p of patterns) {
|
|
3264
|
+
matches.push(...findMatches(
|
|
3265
|
+
content,
|
|
3266
|
+
p,
|
|
3267
|
+
largeBundleImport,
|
|
3268
|
+
filePath,
|
|
3269
|
+
() => "Import only what you need: import { debounce } from 'lodash/debounce'. Or switch to lighter alternatives like date-fns instead of moment."
|
|
3270
|
+
));
|
|
3271
|
+
}
|
|
3272
|
+
return matches;
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3275
|
+
var blockingMainThread = {
|
|
3276
|
+
id: "VC102",
|
|
3277
|
+
title: "Blocking Main Thread with Heavy Computation",
|
|
3278
|
+
severity: "medium",
|
|
3279
|
+
category: "Performance",
|
|
3280
|
+
description: "Infinite loops or deeply nested iterations on the main thread freeze the UI and cause unresponsiveness.",
|
|
3281
|
+
check(content, filePath) {
|
|
3282
|
+
if (filePath.match(/worker|test|spec|mock/i)) return [];
|
|
3283
|
+
const matches = [];
|
|
3284
|
+
if (/while\s*\(\s*true\s*\)/g.test(content)) {
|
|
3285
|
+
matches.push(...findMatches(
|
|
3286
|
+
content,
|
|
3287
|
+
/while\s*\(\s*true\s*\)/g,
|
|
3288
|
+
blockingMainThread,
|
|
3289
|
+
filePath,
|
|
3290
|
+
() => "Avoid while(true) on the main thread. Use Web Workers for heavy computation or requestIdleCallback for non-urgent work."
|
|
3291
|
+
));
|
|
3292
|
+
}
|
|
3293
|
+
return matches;
|
|
3294
|
+
}
|
|
3295
|
+
};
|
|
3296
|
+
var todoLeftInCode = {
|
|
3297
|
+
id: "VC103",
|
|
3298
|
+
title: "TODO/FIXME Left in Code",
|
|
3299
|
+
severity: "low",
|
|
3300
|
+
category: "Code Quality",
|
|
3301
|
+
description: "TODO, FIXME, HACK, and XXX comments indicate unfinished work that should be resolved before shipping to production.",
|
|
3302
|
+
check(content, filePath) {
|
|
3303
|
+
if (filePath.match(/test|spec|mock|__tests__|fixture|node_modules/i)) return [];
|
|
3304
|
+
return findMatches(
|
|
3305
|
+
content,
|
|
3306
|
+
/\/\/\s*(?:TODO|FIXME|HACK|XXX)\b/gi,
|
|
3307
|
+
todoLeftInCode,
|
|
3308
|
+
filePath,
|
|
3309
|
+
() => "Resolve TODO/FIXME comments before shipping. If it's intentional tech debt, track it in your issue tracker instead."
|
|
3310
|
+
).slice(0, 5);
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
var emptyCatchBlock = {
|
|
3314
|
+
id: "VC104",
|
|
3315
|
+
title: "Empty Catch Block",
|
|
3316
|
+
severity: "medium",
|
|
3317
|
+
category: "Code Quality",
|
|
3318
|
+
description: "Empty catch blocks silently swallow errors, making bugs impossible to diagnose. At minimum, log the error.",
|
|
3319
|
+
check(content, filePath) {
|
|
3320
|
+
if (filePath.match(/test|spec|mock/i)) return [];
|
|
3321
|
+
return findMatches(
|
|
3322
|
+
content,
|
|
3323
|
+
/catch\s*(?:\([^)]*\))?\s*\{\s*\}/g,
|
|
3324
|
+
emptyCatchBlock,
|
|
3325
|
+
filePath,
|
|
3326
|
+
() => "Handle errors in catch blocks: catch(err) { console.error('Operation failed:', err); } or re-throw if appropriate."
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
var callbackHell = {
|
|
3331
|
+
id: "VC105",
|
|
3332
|
+
title: "Deeply Nested Callbacks (Promise Chain)",
|
|
3333
|
+
severity: "medium",
|
|
3334
|
+
category: "Code Quality",
|
|
3335
|
+
description: "Long .then() chains or deeply nested callbacks are hard to read, debug, and maintain. Refactor to async/await.",
|
|
3336
|
+
check(content, filePath) {
|
|
3337
|
+
if (filePath.match(/test|spec|mock/i)) return [];
|
|
3338
|
+
return findMatches(
|
|
3339
|
+
content,
|
|
3340
|
+
/\.then\s*\([^)]*\)\s*\.then\s*\([^)]*\)\s*\.then/g,
|
|
3341
|
+
callbackHell,
|
|
3342
|
+
filePath,
|
|
3343
|
+
() => "Refactor .then() chains to async/await for cleaner, more readable code."
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
var magicNumbers = {
|
|
3348
|
+
id: "VC106",
|
|
3349
|
+
title: "Magic Numbers in Code",
|
|
3350
|
+
severity: "low",
|
|
3351
|
+
category: "Code Quality",
|
|
3352
|
+
description: "Unnamed numeric constants in conditions or calculations make code hard to understand. Extract them into named constants.",
|
|
3353
|
+
check(content, filePath) {
|
|
3354
|
+
if (filePath.match(/test|spec|mock|config|\.config\.|constant|enum|migration/i)) return [];
|
|
3355
|
+
if (!filePath.match(/\.(jsx?|tsx?)$/)) return [];
|
|
3356
|
+
const matches = [];
|
|
3357
|
+
const lines = content.split("\n");
|
|
3358
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3359
|
+
const line = lines[i].trim();
|
|
3360
|
+
if (line.startsWith("//") || line.startsWith("*")) continue;
|
|
3361
|
+
if (/(?:===|!==|>=?|<=?)\s*\d{3,}/.test(line) || /(?:setTimeout|setInterval)\s*\([^,]+,\s*\d{4,}/.test(line)) {
|
|
3362
|
+
matches.push({ rule: "VC106", title: magicNumbers.title, severity: "low", category: "Code Quality", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Extract magic numbers into named constants: const MAX_RETRIES = 3; const TIMEOUT_MS = 5000;" });
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
return matches.slice(0, 3);
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3135
3368
|
var allRules = [
|
|
3136
3369
|
hardcodedSecrets,
|
|
3137
3370
|
exposedEnvFile,
|
|
@@ -3228,7 +3461,17 @@ var allRules = [
|
|
|
3228
3461
|
unprotectedDownload,
|
|
3229
3462
|
commandInjection,
|
|
3230
3463
|
corsLocalhost,
|
|
3231
|
-
insecureGRPC
|
|
3464
|
+
insecureGRPC,
|
|
3465
|
+
consoleLogProduction,
|
|
3466
|
+
syncFileOps,
|
|
3467
|
+
eventListenerLeak,
|
|
3468
|
+
nPlusOneQuery,
|
|
3469
|
+
largeBundleImport,
|
|
3470
|
+
blockingMainThread,
|
|
3471
|
+
todoLeftInCode,
|
|
3472
|
+
emptyCatchBlock,
|
|
3473
|
+
callbackHell,
|
|
3474
|
+
magicNumbers
|
|
3232
3475
|
];
|
|
3233
3476
|
function runCustomRules(content, filePath, disabledRules = []) {
|
|
3234
3477
|
const findings = [];
|
|
@@ -4060,9 +4303,19 @@ var SAFE_PATTERNS = [
|
|
|
4060
4303
|
// DOCTYPE/DTD URLs
|
|
4061
4304
|
/DTD|DOCTYPE|w3\.org|apple\.com\/DTDs/i,
|
|
4062
4305
|
// XML namespaces
|
|
4063
|
-
/xmlns|schema|xsd|xsi/i
|
|
4306
|
+
/xmlns|schema|xsd|xsi/i,
|
|
4307
|
+
// npm/package registry URLs
|
|
4308
|
+
/^https?:\/\/registry\.npmjs\.org\//,
|
|
4309
|
+
/^https?:\/\/registry\.yarnpkg\.com\//,
|
|
4310
|
+
// Package integrity hashes (sha512-..., sha256-...)
|
|
4311
|
+
/^sha\d+-[A-Za-z0-9+/=]+$/,
|
|
4312
|
+
// Resolved package URLs (.tgz)
|
|
4313
|
+
/\.tgz$/,
|
|
4314
|
+
// npm resolved URLs (any registry URL with package tarball)
|
|
4315
|
+
/registry.*\/-\/.*\.tgz$/
|
|
4064
4316
|
];
|
|
4065
4317
|
var SKIP_FILES = /\.(css|scss|less|svg|md|txt|html?|xml|yml|yaml|toml|lock|map|woff2?|ttf|eot|ico|png|jpg|gif|webp)$/i;
|
|
4318
|
+
var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
|
|
4066
4319
|
var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset)/i;
|
|
4067
4320
|
function getSnippet3(content, line) {
|
|
4068
4321
|
const lines = content.split("\n");
|
|
@@ -4078,6 +4331,7 @@ function scanEntropy(files) {
|
|
|
4078
4331
|
const findings = [];
|
|
4079
4332
|
for (const { path: filePath, content } of files) {
|
|
4080
4333
|
if (SKIP_FILES.test(filePath)) continue;
|
|
4334
|
+
if (SKIP_FILENAMES.test(filePath)) continue;
|
|
4081
4335
|
if (filePath.includes("node_modules")) continue;
|
|
4082
4336
|
if (filePath.includes(".min.")) continue;
|
|
4083
4337
|
if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
|