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 CHANGED
@@ -335,15 +335,21 @@ var hardcodedSecrets = {
335
335
  ];
336
336
  const matches = [];
337
337
  for (const pattern of patterns) {
338
- matches.push(
339
- ...findMatches(
340
- content,
341
- pattern,
342
- hardcodedSecrets,
343
- filePath,
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 (/import.*DOMPurify|require.*DOMPurify|from\s+['"]dompurify/i.test(content)) return [];
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 = /DOMPurify|sanitize|escapeHtml|escapeHTML|xss|htmlEncode|purify/i.test(content);
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
- matches.push(...findMatches(
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
- if (!filePath.match(/(?:layout|_app|main|index)\.[jt]sx?$/) || filePath.match(/App\.[jt]sx?$/)) return [];
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 (/(?:main\/main|main\.ts$|main\.js$)/.test(filePath) && /(?:electron|BrowserWindow)/i.test(content)) return [];
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 (!/(?:React|react-dom|createRoot|ReactDOM|jsx|tsx)/i.test(content)) return [];
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|<[A-Z]/i.test(content)) {
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
- matches.push(...findMatches(
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
- return findMatches(
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;