xploitscan 0.4.0 → 0.6.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,7 +3168,547 @@ var complianceMap = {
3132
3168
  VC095: { owasp: "A05:2021", cwe: "CWE-942" },
3133
3169
  VC096: { owasp: "A02:2021", cwe: "CWE-319" }
3134
3170
  };
3135
- var allRules = [
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
+ };
3368
+ var s3BucketNoEncryption = {
3369
+ id: "VC107",
3370
+ title: "S3 Bucket Without Encryption",
3371
+ severity: "high",
3372
+ category: "Infrastructure",
3373
+ description: "AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.",
3374
+ check(content, filePath) {
3375
+ if (!filePath.match(/\.tf$/)) return [];
3376
+ if (!/resource\s+"aws_s3_bucket"/.test(content)) return [];
3377
+ const matches = [];
3378
+ const bucketPattern = /resource\s+"aws_s3_bucket"\s+"(\w+)"/g;
3379
+ let m;
3380
+ while ((m = bucketPattern.exec(content)) !== null) {
3381
+ if (isCommentLine(content, m.index)) continue;
3382
+ const blockEnd = Math.min(m.index + 2e3, content.length);
3383
+ const blockContent = content.substring(m.index, blockEnd);
3384
+ if (!/server_side_encryption/.test(blockContent)) {
3385
+ const lineNum = content.substring(0, m.index).split("\n").length;
3386
+ matches.push({
3387
+ rule: "VC107",
3388
+ title: s3BucketNoEncryption.title,
3389
+ severity: "high",
3390
+ category: "Infrastructure",
3391
+ file: filePath,
3392
+ line: lineNum,
3393
+ snippet: getSnippet(content, lineNum),
3394
+ fix: "Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }"
3395
+ });
3396
+ }
3397
+ }
3398
+ return matches;
3399
+ }
3400
+ };
3401
+ var securityGroupAllInbound = {
3402
+ id: "VC108",
3403
+ title: "Security Group Allows All Inbound",
3404
+ severity: "critical",
3405
+ category: "Infrastructure",
3406
+ description: "Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.",
3407
+ check(content, filePath) {
3408
+ if (!filePath.match(/\.(tf|json|yaml|yml)$/)) return [];
3409
+ const matches = [];
3410
+ if (filePath.match(/\.tf$/)) {
3411
+ const ingressPattern = /ingress\s*\{[^}]*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\][^}]*\}/gs;
3412
+ let m;
3413
+ while ((m = ingressPattern.exec(content)) !== null) {
3414
+ if (isCommentLine(content, m.index)) continue;
3415
+ if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
3416
+ const lineNum = content.substring(0, m.index).split("\n").length;
3417
+ matches.push({
3418
+ rule: "VC108",
3419
+ title: securityGroupAllInbound.title,
3420
+ severity: "critical",
3421
+ category: "Infrastructure",
3422
+ file: filePath,
3423
+ line: lineNum,
3424
+ snippet: getSnippet(content, lineNum),
3425
+ fix: "Restrict security group ingress to specific IP ranges and ports."
3426
+ });
3427
+ }
3428
+ }
3429
+ }
3430
+ if (filePath.match(/\.(json|yaml|yml)$/)) {
3431
+ const cfnPattern = /AWS::EC2::SecurityGroup/g;
3432
+ if (cfnPattern.test(content) && /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/.test(content)) {
3433
+ matches.push(...findMatches(
3434
+ content,
3435
+ /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/g,
3436
+ securityGroupAllInbound,
3437
+ filePath,
3438
+ () => "Restrict security group ingress to specific IP ranges and ports."
3439
+ ));
3440
+ }
3441
+ }
3442
+ return matches;
3443
+ }
3444
+ };
3445
+ var rdsPubliclyAccessible = {
3446
+ id: "VC109",
3447
+ title: "RDS Instance Publicly Accessible",
3448
+ severity: "critical",
3449
+ category: "Infrastructure",
3450
+ description: "RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.",
3451
+ check(content, filePath) {
3452
+ if (!filePath.match(/\.tf$/)) return [];
3453
+ if (!/resource\s+"aws_db_instance"/.test(content)) return [];
3454
+ return findMatches(
3455
+ content,
3456
+ /publicly_accessible\s*=\s*true/g,
3457
+ rdsPubliclyAccessible,
3458
+ filePath,
3459
+ () => "Set publicly_accessible = false. Access RDS through VPC private subnets."
3460
+ );
3461
+ }
3462
+ };
3463
+ var missingCloudTrail = {
3464
+ id: "VC110",
3465
+ title: "Missing CloudTrail Logging",
3466
+ severity: "high",
3467
+ category: "Infrastructure",
3468
+ description: "AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.",
3469
+ check(content, filePath) {
3470
+ if (!filePath.match(/\.tf$/)) return [];
3471
+ if (!/provider\s+"aws"/.test(content)) return [];
3472
+ if (/aws_cloudtrail/.test(content)) return [];
3473
+ const lineNum = content.substring(0, content.search(/provider\s+"aws"/)).split("\n").length;
3474
+ return [{
3475
+ rule: "VC110",
3476
+ title: missingCloudTrail.title,
3477
+ severity: "high",
3478
+ category: "Infrastructure",
3479
+ file: filePath,
3480
+ line: lineNum,
3481
+ snippet: getSnippet(content, lineNum),
3482
+ fix: "Enable CloudTrail for audit logging of all AWS API calls."
3483
+ }];
3484
+ }
3485
+ };
3486
+ var lambdaWithoutVPC = {
3487
+ id: "VC111",
3488
+ title: "Lambda Without VPC",
3489
+ severity: "medium",
3490
+ category: "Infrastructure",
3491
+ description: "Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.",
3492
+ check(content, filePath) {
3493
+ if (!filePath.match(/\.tf$/)) return [];
3494
+ if (!/resource\s+"aws_lambda_function"/.test(content)) return [];
3495
+ const matches = [];
3496
+ const lambdaPattern = /resource\s+"aws_lambda_function"\s+"(\w+)"/g;
3497
+ let m;
3498
+ while ((m = lambdaPattern.exec(content)) !== null) {
3499
+ if (isCommentLine(content, m.index)) continue;
3500
+ const blockEnd = Math.min(m.index + 2e3, content.length);
3501
+ const blockContent = content.substring(m.index, blockEnd);
3502
+ if (!/vpc_config\s*\{/.test(blockContent)) {
3503
+ const lineNum = content.substring(0, m.index).split("\n").length;
3504
+ matches.push({
3505
+ rule: "VC111",
3506
+ title: lambdaWithoutVPC.title,
3507
+ severity: "medium",
3508
+ category: "Infrastructure",
3509
+ file: filePath,
3510
+ line: lineNum,
3511
+ snippet: getSnippet(content, lineNum),
3512
+ fix: "Place Lambda functions in a VPC for network isolation."
3513
+ });
3514
+ }
3515
+ }
3516
+ return matches;
3517
+ }
3518
+ };
3519
+ var dockerLatestTag = {
3520
+ id: "VC112",
3521
+ title: "Docker Image Using Latest Tag",
3522
+ severity: "high",
3523
+ category: "Configuration",
3524
+ description: "Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.",
3525
+ check(content, filePath) {
3526
+ if (!filePath.match(/Dockerfile$/i)) return [];
3527
+ const matches = [];
3528
+ const fromPattern = /^FROM\s+(?!scratch)(\S+?)(?:\s+AS\s+\S+)?\s*$/gm;
3529
+ let m;
3530
+ while ((m = fromPattern.exec(content)) !== null) {
3531
+ const image = m[1];
3532
+ if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
3533
+ const lineNum = content.substring(0, m.index).split("\n").length;
3534
+ matches.push({
3535
+ rule: "VC112",
3536
+ title: dockerLatestTag.title,
3537
+ severity: "high",
3538
+ category: "Configuration",
3539
+ file: filePath,
3540
+ line: lineNum,
3541
+ snippet: getSnippet(content, lineNum),
3542
+ fix: "Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest"
3543
+ });
3544
+ }
3545
+ }
3546
+ return matches;
3547
+ }
3548
+ };
3549
+ var dockerCopySensitive = {
3550
+ id: "VC113",
3551
+ title: "Docker COPY With Sensitive Files",
3552
+ severity: "high",
3553
+ category: "Configuration",
3554
+ description: "Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.",
3555
+ check(content, filePath) {
3556
+ if (!filePath.match(/Dockerfile$/i)) return [];
3557
+ if (!/(?:COPY|ADD)\s+\.\s+\./.test(content)) return [];
3558
+ return findMatches(
3559
+ content,
3560
+ /(?:COPY|ADD)\s+\.\s+\./g,
3561
+ dockerCopySensitive,
3562
+ filePath,
3563
+ () => "Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context."
3564
+ );
3565
+ }
3566
+ };
3567
+ var dockerTooManyPorts = {
3568
+ id: "VC114",
3569
+ title: "Docker Exposing Too Many Ports",
3570
+ severity: "medium",
3571
+ category: "Configuration",
3572
+ description: "Exposing many ports or wide port ranges increases the attack surface of a container.",
3573
+ check(content, filePath) {
3574
+ if (!filePath.match(/Dockerfile$/i)) return [];
3575
+ const matches = [];
3576
+ const rangePattern = /^EXPOSE\s+\d+-\d+/gm;
3577
+ matches.push(...findMatches(
3578
+ content,
3579
+ rangePattern,
3580
+ dockerTooManyPorts,
3581
+ filePath,
3582
+ () => "Only expose necessary ports. Each EXPOSE should have a clear purpose."
3583
+ ));
3584
+ const exposeLines = content.split("\n").filter((l) => /^\s*EXPOSE\s+/.test(l));
3585
+ if (exposeLines.length > 3) {
3586
+ const firstExpose = content.search(/^\s*EXPOSE\s+/m);
3587
+ if (firstExpose >= 0) {
3588
+ const lineNum = content.substring(0, firstExpose).split("\n").length;
3589
+ matches.push({
3590
+ rule: "VC114",
3591
+ title: dockerTooManyPorts.title,
3592
+ severity: "medium",
3593
+ category: "Configuration",
3594
+ file: filePath,
3595
+ line: lineNum,
3596
+ snippet: getSnippet(content, lineNum),
3597
+ fix: "Only expose necessary ports. Each EXPOSE should have a clear purpose."
3598
+ });
3599
+ }
3600
+ }
3601
+ return matches;
3602
+ }
3603
+ };
3604
+ var k8sSecretNotEncrypted = {
3605
+ id: "VC115",
3606
+ title: "Kubernetes Secret Not Encrypted",
3607
+ severity: "high",
3608
+ category: "Infrastructure",
3609
+ description: "Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.",
3610
+ check(content, filePath) {
3611
+ if (!filePath.match(/\.(yaml|yml)$/)) return [];
3612
+ if (!/kind\s*:\s*Secret/i.test(content)) return [];
3613
+ if (/sealedsecrets\.bitnami\.com|external-secrets\.io/i.test(content)) return [];
3614
+ return findMatches(
3615
+ content,
3616
+ /kind\s*:\s*Secret/g,
3617
+ k8sSecretNotEncrypted,
3618
+ filePath,
3619
+ () => "Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests."
3620
+ );
3621
+ }
3622
+ };
3623
+ var k8sNoResourceLimits = {
3624
+ id: "VC116",
3625
+ title: "Kubernetes Pod Without Resource Limits",
3626
+ severity: "medium",
3627
+ category: "Infrastructure",
3628
+ description: "Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.",
3629
+ check(content, filePath) {
3630
+ if (!filePath.match(/\.(yaml|yml)$/)) return [];
3631
+ if (!/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];
3632
+ if (/resources\s*:\s*\n\s+limits\s*:/m.test(content)) return [];
3633
+ if (/resources\s*:.*limits/i.test(content)) return [];
3634
+ const kindMatch = content.match(/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);
3635
+ if (!kindMatch) return [];
3636
+ const lineNum = content.substring(0, kindMatch.index).split("\n").length;
3637
+ return [{
3638
+ rule: "VC116",
3639
+ title: k8sNoResourceLimits.title,
3640
+ severity: "medium",
3641
+ category: "Infrastructure",
3642
+ file: filePath,
3643
+ line: lineNum,
3644
+ snippet: getSnippet(content, lineNum),
3645
+ fix: "Set resource limits to prevent pods from consuming excessive CPU/memory."
3646
+ }];
3647
+ }
3648
+ };
3649
+ var freeRules = [
3650
+ hardcodedSecrets,
3651
+ // VC001
3652
+ exposedEnvFile,
3653
+ // VC002
3654
+ missingAuthMiddleware,
3655
+ // VC003
3656
+ supabaseNoRLS,
3657
+ // VC004
3658
+ stripeWebhookUnprotected,
3659
+ // VC005
3660
+ sqlInjection,
3661
+ // VC006
3662
+ xssVulnerability,
3663
+ // VC007
3664
+ noRateLimiting,
3665
+ // VC008
3666
+ corsWildcard,
3667
+ // VC009
3668
+ clientSideAuth,
3669
+ // VC010
3670
+ nextPublicSecret,
3671
+ // VC011
3672
+ envNotGitignored,
3673
+ // VC014
3674
+ evalUsage,
3675
+ // VC015
3676
+ unvalidatedRedirect,
3677
+ // VC016
3678
+ insecureCookies,
3679
+ // VC017
3680
+ exposedAuthSecret,
3681
+ // VC018
3682
+ missingCSP,
3683
+ // VC020
3684
+ hardcodedJWTSecret,
3685
+ // VC031
3686
+ missingHTTPS,
3687
+ // VC032
3688
+ exposedDebugMode,
3689
+ // VC033
3690
+ insecureRandomness,
3691
+ // VC034
3692
+ missingErrorBoundary,
3693
+ // VC036
3694
+ exposedStackTraces,
3695
+ // VC037
3696
+ missingLockFile,
3697
+ // VC039
3698
+ dangerousInnerHTML,
3699
+ // VC063
3700
+ consoleLogProduction,
3701
+ // VC097
3702
+ emptyCatchBlock,
3703
+ // VC104
3704
+ todoLeftInCode,
3705
+ // VC103
3706
+ weakHashing,
3707
+ // VC060
3708
+ disabledTLSVerification
3709
+ // VC061
3710
+ ];
3711
+ var proRules = [
3136
3712
  hardcodedSecrets,
3137
3713
  exposedEnvFile,
3138
3714
  missingAuthMiddleware,
@@ -3228,14 +3804,35 @@ var allRules = [
3228
3804
  unprotectedDownload,
3229
3805
  commandInjection,
3230
3806
  corsLocalhost,
3231
- insecureGRPC
3807
+ insecureGRPC,
3808
+ consoleLogProduction,
3809
+ syncFileOps,
3810
+ eventListenerLeak,
3811
+ nPlusOneQuery,
3812
+ largeBundleImport,
3813
+ blockingMainThread,
3814
+ todoLeftInCode,
3815
+ emptyCatchBlock,
3816
+ callbackHell,
3817
+ magicNumbers,
3818
+ s3BucketNoEncryption,
3819
+ securityGroupAllInbound,
3820
+ rdsPubliclyAccessible,
3821
+ missingCloudTrail,
3822
+ lambdaWithoutVPC,
3823
+ dockerLatestTag,
3824
+ dockerCopySensitive,
3825
+ dockerTooManyPorts,
3826
+ k8sSecretNotEncrypted,
3827
+ k8sNoResourceLimits
3232
3828
  ];
3233
- function runCustomRules(content, filePath, disabledRules = []) {
3829
+ function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
3234
3830
  const findings = [];
3235
3831
  if (/function runScan\(files\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\s*[:=]/.test(content) && /findMatches/.test(content)) {
3236
3832
  return findings;
3237
3833
  }
3238
- for (const rule of allRules) {
3834
+ const ruleset = tier === "pro" ? proRules : freeRules;
3835
+ for (const rule of ruleset) {
3239
3836
  if (disabledRules.includes(rule.id)) continue;
3240
3837
  const matches = rule.check(content, filePath);
3241
3838
  const compliance = complianceMap[rule.id];
@@ -4060,9 +4657,19 @@ var SAFE_PATTERNS = [
4060
4657
  // DOCTYPE/DTD URLs
4061
4658
  /DTD|DOCTYPE|w3\.org|apple\.com\/DTDs/i,
4062
4659
  // XML namespaces
4063
- /xmlns|schema|xsd|xsi/i
4660
+ /xmlns|schema|xsd|xsi/i,
4661
+ // npm/package registry URLs
4662
+ /^https?:\/\/registry\.npmjs\.org\//,
4663
+ /^https?:\/\/registry\.yarnpkg\.com\//,
4664
+ // Package integrity hashes (sha512-..., sha256-...)
4665
+ /^sha\d+-[A-Za-z0-9+/=]+$/,
4666
+ // Resolved package URLs (.tgz)
4667
+ /\.tgz$/,
4668
+ // npm resolved URLs (any registry URL with package tarball)
4669
+ /registry.*\/-\/.*\.tgz$/
4064
4670
  ];
4065
4671
  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;
4672
+ 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
4673
  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
4674
  function getSnippet3(content, line) {
4068
4675
  const lines = content.split("\n");
@@ -4078,6 +4685,7 @@ function scanEntropy(files) {
4078
4685
  const findings = [];
4079
4686
  for (const { path: filePath, content } of files) {
4080
4687
  if (SKIP_FILES.test(filePath)) continue;
4688
+ if (SKIP_FILENAMES.test(filePath)) continue;
4081
4689
  if (filePath.includes("node_modules")) continue;
4082
4690
  if (filePath.includes(".min.")) continue;
4083
4691
  if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;