xploitscan 1.0.14 → 1.0.16

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
@@ -270,7 +270,18 @@ async function loadConfig(directory) {
270
270
  return config;
271
271
  }
272
272
 
273
- // src/scanners/custom-rules.ts
273
+ // ../shared-rules/dist/index.js
274
+ import Anthropic from "@anthropic-ai/sdk";
275
+ function getSnippet2(content, line, contextLines = 2) {
276
+ const lines = content.split("\n");
277
+ const start = Math.max(0, line - 1 - contextLines);
278
+ const end = Math.min(lines.length, line + contextLines);
279
+ return lines.slice(start, end).map((l, i) => {
280
+ const lineNum = start + i + 1;
281
+ const marker = lineNum === line ? ">" : " ";
282
+ return `${marker} ${lineNum.toString().padStart(4)} | ${l}`;
283
+ }).join("\n");
284
+ }
274
285
  var TEST_FILE_PATTERN = /(?:\.test\.|\.spec\.|__tests__|__mocks__|\.stories\.|\.story\.|\/test\/|\/tests\/|\/fixtures?\/|\/mocks?\/|\.mock\.|test-utils|testing|\.cy\.|\.e2e\.)/i;
275
286
  function isTestFile(filePath) {
276
287
  return TEST_FILE_PATTERN.test(filePath);
@@ -301,7 +312,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
301
312
  category: rule.category,
302
313
  file: filePath,
303
314
  line: lineNum,
304
- snippet: getSnippet(content, lineNum),
315
+ snippet: getSnippet2(content, lineNum),
305
316
  fix: fixTemplate?.(m)
306
317
  });
307
318
  }
@@ -378,7 +389,7 @@ var exposedEnvFile = {
378
389
  category: "Secrets",
379
390
  file: filePath,
380
391
  line: 1,
381
- snippet: getSnippet(content, 1),
392
+ snippet: getSnippet2(content, 1),
382
393
  fix: 'Add ".env*" to your .gitignore file and remove this file from git history with: git rm --cached ' + filePath
383
394
  }];
384
395
  }
@@ -614,7 +625,7 @@ var noRateLimiting = {
614
625
  category: "Availability",
615
626
  file: filePath,
616
627
  line: 1,
617
- snippet: getSnippet(content, 1),
628
+ snippet: getSnippet2(content, 1),
618
629
  fix: "Add rate limiting middleware to your server. For Express: npm install express-rate-limit. For other frameworks, check their rate limiting plugins."
619
630
  }];
620
631
  }
@@ -727,7 +738,7 @@ var envNotGitignored = {
727
738
  category: "Secrets",
728
739
  file: filePath,
729
740
  line: 1,
730
- snippet: getSnippet(content, 1),
741
+ snippet: getSnippet2(content, 1),
731
742
  fix: 'Add ".env*" to your .gitignore file to prevent committing secrets.'
732
743
  }];
733
744
  }
@@ -885,7 +896,7 @@ var missingCSP = {
885
896
  category: "Configuration",
886
897
  file: filePath,
887
898
  line: 1,
888
- snippet: getSnippet(content, 1),
899
+ snippet: getSnippet2(content, 1),
889
900
  fix: `Add a CSP meta tag: <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">`
890
901
  }];
891
902
  }
@@ -1034,7 +1045,7 @@ var missingErrorBoundary = {
1034
1045
  category: "Configuration",
1035
1046
  file: filePath,
1036
1047
  line: 1,
1037
- snippet: getSnippet(content, 1),
1048
+ snippet: getSnippet2(content, 1),
1038
1049
  fix: "Wrap your app in an ErrorBoundary component to catch rendering errors gracefully. Use react-error-boundary or create a class component with componentDidCatch."
1039
1050
  }];
1040
1051
  }
@@ -1179,7 +1190,7 @@ var dangerousInnerHTML = {
1179
1190
  category: "Injection",
1180
1191
  file: filePath,
1181
1192
  line: lineNum,
1182
- snippet: getSnippet(content, lineNum),
1193
+ snippet: getSnippet2(content, lineNum),
1183
1194
  fix: "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
1184
1195
  });
1185
1196
  }
@@ -1431,7 +1442,7 @@ var consoleLogProduction = {
1431
1442
  for (let i = 0; i < lines.length; i++) {
1432
1443
  const line = lines[i].trim();
1433
1444
  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)) {
1434
- matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "low", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet(content, i + 1), fix: "Remove console.log or use a structured logger that can be disabled in production." });
1445
+ matches.push({ rule: "VC097", title: consoleLogProduction.title, severity: "low", category: "Performance", file: filePath, line: i + 1, snippet: getSnippet2(content, i + 1), fix: "Remove console.log or use a structured logger that can be disabled in production." });
1435
1446
  }
1436
1447
  }
1437
1448
  return matches.slice(0, 1);
@@ -1566,6 +1577,131 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
1566
1577
  }
1567
1578
  return findings;
1568
1579
  }
1580
+ var REVIEW_SYSTEM_PROMPT = `You are reviewing security scan findings for false positives. Your job is to look at each finding and the surrounding code to determine if it's a REAL security vulnerability or a FALSE POSITIVE.
1581
+
1582
+ Common false positive patterns you should catch:
1583
+ - Auth check exists inside the function body (requireUser, requireUserForApi, getSession, etc.) but the scanner only checked the function signature
1584
+ - The flagged pattern is in example/documentation/tutorial code, not production code
1585
+ - The variable is developer-controlled (constants, config values, static strings), not user input
1586
+ - The flagged function is a database method (conn.exec, db.exec, prisma.$executeRaw) not a shell command (child_process.exec)
1587
+ - The comparison is a type check (typeof x === "string") not a secret comparison
1588
+ - The file is a test, mock, or fixture file
1589
+ - The "secret" is a publishable/public key (pk_test_, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) designed to be client-side
1590
+ - The innerHTML/dangerouslySetInnerHTML uses a static constant or JSON.stringify, not user input
1591
+ - The redirect URL has already been validated (isAllowedRedirect, validateRedirect)
1592
+ - The webhook endpoint is for a non-Stripe service but flagged as "Stripe webhook"
1593
+ - The "sensitive data in URL" is in a comment or documentation, not actual code
1594
+ - Package-lock.json URLs flagged as secrets (they're npm registry URLs, not secrets)
1595
+
1596
+ For each finding, respond ONLY with a JSON array. No other text.
1597
+ Each element: {"index": <number>, "verdict": "real" or "fp", "reason": "<1 sentence>"}`;
1598
+ var MAX_FINDINGS_PER_BATCH = 15;
1599
+ var MAX_CONTEXT_LINES = 10;
1600
+ var MAX_TOTAL_FINDINGS = 50;
1601
+ function getExpandedContext(content, line, contextLines = MAX_CONTEXT_LINES) {
1602
+ const lines = content.split("\n");
1603
+ const start = Math.max(0, line - 1 - contextLines);
1604
+ const end = Math.min(lines.length, line + contextLines);
1605
+ return lines.slice(start, end).map((l, i) => {
1606
+ const lineNum = start + i + 1;
1607
+ const marker = lineNum === line ? ">>>" : " ";
1608
+ return `${marker} ${lineNum} | ${l}`;
1609
+ }).join("\n");
1610
+ }
1611
+ function buildReviewPrompt(findings, fileContent) {
1612
+ const parts = [];
1613
+ for (let i = 0; i < findings.length; i++) {
1614
+ const f = findings[i];
1615
+ const context = getExpandedContext(fileContent, f.line);
1616
+ parts.push(`--- Finding ${i} ---
1617
+ Rule: ${f.rule} (${f.title})
1618
+ Severity: ${f.severity}
1619
+ File: ${f.file}
1620
+ Line: ${f.line}
1621
+ Description: ${f.description}
1622
+ Suggested fix: ${f.fix || "N/A"}
1623
+
1624
+ Code context:
1625
+ ${context}
1626
+ `);
1627
+ }
1628
+ return `Review these ${findings.length} security scan findings. For each one, determine if it's a real vulnerability or a false positive based on the code context.
1629
+
1630
+ ${parts.join("\n")}`;
1631
+ }
1632
+ function parseReviewResponse(text) {
1633
+ try {
1634
+ const cleaned = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
1635
+ const parsed = JSON.parse(cleaned);
1636
+ if (!Array.isArray(parsed)) return [];
1637
+ return parsed.filter(
1638
+ (r) => typeof r === "object" && r !== null && "index" in r && "verdict" in r && (r.verdict === "real" || r.verdict === "fp")
1639
+ );
1640
+ } catch {
1641
+ return [];
1642
+ }
1643
+ }
1644
+ async function filterFalsePositives(findings, fileContents) {
1645
+ const empty = { findings, filteredFindings: [], aiReviewed: false, removedCount: 0, totalBefore: findings.length };
1646
+ if (!process.env.ANTHROPIC_API_KEY) return empty;
1647
+ if (findings.length === 0) return empty;
1648
+ const toReview = findings.slice(0, MAX_TOTAL_FINDINGS);
1649
+ const overflow = findings.slice(MAX_TOTAL_FINDINGS);
1650
+ const totalBefore = findings.length;
1651
+ const byFile = /* @__PURE__ */ new Map();
1652
+ for (const f of toReview) {
1653
+ const group = byFile.get(f.file) || [];
1654
+ group.push(f);
1655
+ byFile.set(f.file, group);
1656
+ }
1657
+ let client;
1658
+ try {
1659
+ client = new Anthropic();
1660
+ } catch {
1661
+ return empty;
1662
+ }
1663
+ const fpMap = /* @__PURE__ */ new Map();
1664
+ for (const [file, fileFindings] of byFile) {
1665
+ const content = fileContents.get(file);
1666
+ if (!content) continue;
1667
+ for (let i = 0; i < fileFindings.length; i += MAX_FINDINGS_PER_BATCH) {
1668
+ const batch = fileFindings.slice(i, i + MAX_FINDINGS_PER_BATCH);
1669
+ const prompt = buildReviewPrompt(batch, content);
1670
+ try {
1671
+ const response = await client.messages.create({
1672
+ model: "claude-haiku-4-5-20251001",
1673
+ max_tokens: 1024,
1674
+ system: REVIEW_SYSTEM_PROMPT,
1675
+ messages: [{ role: "user", content: prompt }]
1676
+ });
1677
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
1678
+ const results = parseReviewResponse(text);
1679
+ for (const r of results) {
1680
+ if (r.verdict === "fp" && r.index >= 0 && r.index < batch.length) {
1681
+ const globalIndex = toReview.indexOf(batch[r.index]);
1682
+ if (globalIndex !== -1) {
1683
+ fpMap.set(globalIndex, r.reason);
1684
+ }
1685
+ }
1686
+ }
1687
+ } catch {
1688
+ continue;
1689
+ }
1690
+ }
1691
+ }
1692
+ const filtered = toReview.filter((_, i) => !fpMap.has(i));
1693
+ const filteredFindings = [];
1694
+ for (const [idx, reason] of fpMap) {
1695
+ filteredFindings.push({ finding: toReview[idx], reason });
1696
+ }
1697
+ return {
1698
+ findings: [...filtered, ...overflow],
1699
+ filteredFindings,
1700
+ aiReviewed: true,
1701
+ removedCount: fpMap.size,
1702
+ totalBefore
1703
+ };
1704
+ }
1569
1705
 
1570
1706
  // src/scanners/semgrep.ts
1571
1707
  import { execFile } from "child_process";
@@ -1791,7 +1927,7 @@ async function runGitleaks(directory) {
1791
1927
  }
1792
1928
 
1793
1929
  // src/scanners/ai-analyzer.ts
1794
- import Anthropic from "@anthropic-ai/sdk";
1930
+ import Anthropic2 from "@anthropic-ai/sdk";
1795
1931
  var SYSTEM_PROMPT = `You are a security auditor specializing in code generated by AI tools (Cursor, Lovable, Bolt, Replit, Claude). Your audience is non-expert developers who may not understand security jargon.
1796
1932
 
1797
1933
  Analyze the provided code for security vulnerabilities. Focus on issues that AI code generators commonly introduce:
@@ -1825,7 +1961,7 @@ Rules:
1825
1961
  async function analyzeWithAI(files, existingFindings) {
1826
1962
  const apiKey = process.env.ANTHROPIC_API_KEY;
1827
1963
  if (!apiKey) return [];
1828
- const client = new Anthropic();
1964
+ const client = new Anthropic2();
1829
1965
  const existingRules = new Set(
1830
1966
  existingFindings.map((f) => `${f.file}:${f.line}:${f.rule}`)
1831
1967
  );
@@ -1916,134 +2052,6 @@ function chunkFiles(files, maxChars) {
1916
2052
  return chunks;
1917
2053
  }
1918
2054
 
1919
- // src/scanners/ai-fp-filter.ts
1920
- import Anthropic2 from "@anthropic-ai/sdk";
1921
- var REVIEW_SYSTEM_PROMPT = `You are reviewing security scan findings for false positives. Your job is to look at each finding and the surrounding code to determine if it's a REAL security vulnerability or a FALSE POSITIVE.
1922
-
1923
- Common false positive patterns you should catch:
1924
- - Auth check exists inside the function body (requireUser, requireUserForApi, getSession, etc.) but the scanner only checked the function signature
1925
- - The flagged pattern is in example/documentation/tutorial code, not production code
1926
- - The variable is developer-controlled (constants, config values, static strings), not user input
1927
- - The flagged function is a database method (conn.exec, db.exec, prisma.$executeRaw) not a shell command (child_process.exec)
1928
- - The comparison is a type check (typeof x === "string") not a secret comparison
1929
- - The file is a test, mock, or fixture file
1930
- - The "secret" is a publishable/public key (pk_test_, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) designed to be client-side
1931
- - The innerHTML/dangerouslySetInnerHTML uses a static constant or JSON.stringify, not user input
1932
- - The redirect URL has already been validated (isAllowedRedirect, validateRedirect)
1933
- - The webhook endpoint is for a non-Stripe service but flagged as "Stripe webhook"
1934
- - The "sensitive data in URL" is in a comment or documentation, not actual code
1935
- - Package-lock.json URLs flagged as secrets (they're npm registry URLs, not secrets)
1936
-
1937
- For each finding, respond ONLY with a JSON array. No other text.
1938
- Each element: {"index": <number>, "verdict": "real" or "fp", "reason": "<1 sentence>"}`;
1939
- var MAX_FINDINGS_PER_BATCH = 15;
1940
- var MAX_CONTEXT_LINES = 10;
1941
- var MAX_TOTAL_FINDINGS = 50;
1942
- function getExpandedContext(content, line, contextLines = MAX_CONTEXT_LINES) {
1943
- const lines = content.split("\n");
1944
- const start = Math.max(0, line - 1 - contextLines);
1945
- const end = Math.min(lines.length, line + contextLines);
1946
- return lines.slice(start, end).map((l, i) => {
1947
- const lineNum = start + i + 1;
1948
- const marker = lineNum === line ? ">>>" : " ";
1949
- return `${marker} ${lineNum} | ${l}`;
1950
- }).join("\n");
1951
- }
1952
- function buildReviewPrompt(findings, fileContent) {
1953
- const parts = [];
1954
- for (let i = 0; i < findings.length; i++) {
1955
- const f = findings[i];
1956
- const context = getExpandedContext(fileContent, f.line);
1957
- parts.push(`--- Finding ${i} ---
1958
- Rule: ${f.rule} (${f.title})
1959
- Severity: ${f.severity}
1960
- File: ${f.file}
1961
- Line: ${f.line}
1962
- Description: ${f.description}
1963
- Suggested fix: ${f.fix || "N/A"}
1964
-
1965
- Code context:
1966
- ${context}
1967
- `);
1968
- }
1969
- return `Review these ${findings.length} security scan findings. For each one, determine if it's a real vulnerability or a false positive based on the code context.
1970
-
1971
- ${parts.join("\n")}`;
1972
- }
1973
- function parseReviewResponse(text) {
1974
- try {
1975
- const cleaned = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
1976
- const parsed = JSON.parse(cleaned);
1977
- if (!Array.isArray(parsed)) return [];
1978
- return parsed.filter(
1979
- (r) => typeof r === "object" && r !== null && "index" in r && "verdict" in r && (r.verdict === "real" || r.verdict === "fp")
1980
- );
1981
- } catch {
1982
- return [];
1983
- }
1984
- }
1985
- async function filterFalsePositives(findings, fileContents) {
1986
- const empty = { findings, filteredFindings: [], aiReviewed: false, removedCount: 0, totalBefore: findings.length };
1987
- if (!process.env.ANTHROPIC_API_KEY) return empty;
1988
- if (findings.length === 0) return empty;
1989
- const toReview = findings.slice(0, MAX_TOTAL_FINDINGS);
1990
- const overflow = findings.slice(MAX_TOTAL_FINDINGS);
1991
- const totalBefore = findings.length;
1992
- const byFile = /* @__PURE__ */ new Map();
1993
- for (const f of toReview) {
1994
- const group = byFile.get(f.file) || [];
1995
- group.push(f);
1996
- byFile.set(f.file, group);
1997
- }
1998
- let client;
1999
- try {
2000
- client = new Anthropic2();
2001
- } catch {
2002
- return empty;
2003
- }
2004
- const fpMap = /* @__PURE__ */ new Map();
2005
- for (const [file, fileFindings] of byFile) {
2006
- const content = fileContents.get(file);
2007
- if (!content) continue;
2008
- for (let i = 0; i < fileFindings.length; i += MAX_FINDINGS_PER_BATCH) {
2009
- const batch = fileFindings.slice(i, i + MAX_FINDINGS_PER_BATCH);
2010
- const prompt = buildReviewPrompt(batch, content);
2011
- try {
2012
- const response = await client.messages.create({
2013
- model: "claude-haiku-4-5-20251001",
2014
- max_tokens: 1024,
2015
- system: REVIEW_SYSTEM_PROMPT,
2016
- messages: [{ role: "user", content: prompt }]
2017
- });
2018
- const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
2019
- const results = parseReviewResponse(text);
2020
- for (const r of results) {
2021
- if (r.verdict === "fp" && r.index >= 0 && r.index < batch.length) {
2022
- const globalIndex = toReview.indexOf(batch[r.index]);
2023
- if (globalIndex !== -1) {
2024
- fpMap.set(globalIndex, r.reason);
2025
- }
2026
- }
2027
- }
2028
- } catch {
2029
- continue;
2030
- }
2031
- }
2032
- }
2033
- const filtered = toReview.filter((_, i) => !fpMap.has(i));
2034
- const filteredFindings = [];
2035
- for (const [idx, reason] of fpMap) {
2036
- filteredFindings.push({ finding: toReview[idx], reason });
2037
- }
2038
- return {
2039
- findings: [...filtered, ...overflow],
2040
- filteredFindings,
2041
- aiReviewed: true,
2042
- removedCount: fpMap.size,
2043
- totalBefore
2044
- };
2045
- }
2046
-
2047
2055
  // src/scanners/ast-analyzer.ts
2048
2056
  function stripComments(content) {
2049
2057
  let result = "";
@@ -2430,7 +2438,7 @@ function compareVersions(v1, v2) {
2430
2438
  }
2431
2439
  return 0;
2432
2440
  }
2433
- function getSnippet2(content, line) {
2441
+ function getSnippet3(content, line) {
2434
2442
  const lines = content.split("\n");
2435
2443
  const start = Math.max(0, line - 2);
2436
2444
  const end = Math.min(lines.length, line + 2);
@@ -2462,7 +2470,7 @@ function scanDependencies(files) {
2462
2470
  description: vuln.description,
2463
2471
  file: filePath,
2464
2472
  line,
2465
- snippet: getSnippet2(content, line),
2473
+ snippet: getSnippet3(content, line),
2466
2474
  fix: vuln.fix,
2467
2475
  category: "Dependencies",
2468
2476
  source: "custom",
@@ -2495,7 +2503,7 @@ function scanDependencies(files) {
2495
2503
  description: vuln.description,
2496
2504
  file: filePath,
2497
2505
  line: i + 1,
2498
- snippet: getSnippet2(content, i + 1),
2506
+ snippet: getSnippet3(content, i + 1),
2499
2507
  fix: vuln.fix,
2500
2508
  category: "Dependencies",
2501
2509
  source: "custom",
@@ -2516,7 +2524,7 @@ function scanDependencies(files) {
2516
2524
  description: pattern.description,
2517
2525
  file: filePath,
2518
2526
  line: 1,
2519
- snippet: getSnippet2(content, 1),
2527
+ snippet: getSnippet3(content, 1),
2520
2528
  fix: pattern.fix,
2521
2529
  category: "Dependencies",
2522
2530
  source: "custom",
@@ -2604,7 +2612,7 @@ async function scanDependenciesOsv(files, alreadyFoundByRule) {
2604
2612
  description: description.slice(0, 500),
2605
2613
  file: lookup.file,
2606
2614
  line: lookup.line,
2607
- snippet: getSnippet2(lookup.content, lookup.line),
2615
+ snippet: getSnippet3(lookup.content, lookup.line),
2608
2616
  fix: `Upgrade ${lookup.name} to a version that resolves ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id}`,
2609
2617
  category: "Dependencies",
2610
2618
  source: "custom",
@@ -2677,7 +2685,7 @@ var SAFE_PATTERNS = [
2677
2685
  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;
2678
2686
  var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
2679
2687
  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;
2680
- function getSnippet3(content, line) {
2688
+ function getSnippet4(content, line) {
2681
2689
  const lines = content.split("\n");
2682
2690
  const start = Math.max(0, line - 2);
2683
2691
  const end = Math.min(lines.length, line + 2);
@@ -2732,7 +2740,7 @@ function scanEntropy(files) {
2732
2740
  description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: "${masked}"`,
2733
2741
  file: filePath,
2734
2742
  line: i + 1,
2735
- snippet: getSnippet3(content, i + 1),
2743
+ snippet: getSnippet4(content, i + 1),
2736
2744
  fix: "If this is a secret, move it to an environment variable. If it's not a secret (e.g., hash, encoded data), add it to .xploitscanignore.",
2737
2745
  category: "Secrets",
2738
2746
  source: "custom",
@@ -2748,7 +2756,7 @@ function scanEntropy(files) {
2748
2756
  }
2749
2757
 
2750
2758
  // src/scanners/config-analyzer.ts
2751
- function getSnippet4(content, line) {
2759
+ function getSnippet5(content, line) {
2752
2760
  const lines = content.split("\n");
2753
2761
  const start = Math.max(0, line - 2);
2754
2762
  const end = Math.min(lines.length, line + 2);
@@ -2774,10 +2782,10 @@ var CONFIG_CHECKS = [
2774
2782
  try {
2775
2783
  if (/"strict"\s*:\s*false/.test(content)) {
2776
2784
  const line = content.split("\n").findIndex((l) => /strict/.test(l)) + 1;
2777
- return { line, snippet: getSnippet4(content, line) };
2785
+ return { line, snippet: getSnippet5(content, line) };
2778
2786
  }
2779
2787
  if (!/"strict"\s*:\s*true/.test(content) && /"compilerOptions"/.test(content)) {
2780
- return { line: 1, snippet: getSnippet4(content, 1) };
2788
+ return { line: 1, snippet: getSnippet5(content, 1) };
2781
2789
  }
2782
2790
  } catch {
2783
2791
  }
@@ -2797,7 +2805,7 @@ var CONFIG_CHECKS = [
2797
2805
  check(content) {
2798
2806
  if (/"allowJs"\s*:\s*true/.test(content) && !/"checkJs"\s*:\s*true/.test(content)) {
2799
2807
  const line = content.split("\n").findIndex((l) => /allowJs/.test(l)) + 1;
2800
- return { line, snippet: getSnippet4(content, line) };
2808
+ return { line, snippet: getSnippet5(content, line) };
2801
2809
  }
2802
2810
  return null;
2803
2811
  }
@@ -2815,7 +2823,7 @@ var CONFIG_CHECKS = [
2815
2823
  filePattern: /next\.config\.(js|mjs|ts)$/,
2816
2824
  check(content) {
2817
2825
  if (!/headers\s*\(/.test(content) && !/securityHeaders|security-headers/.test(content)) {
2818
- return { line: 1, snippet: getSnippet4(content, 1) };
2826
+ return { line: 1, snippet: getSnippet5(content, 1) };
2819
2827
  }
2820
2828
  return null;
2821
2829
  }
@@ -2832,7 +2840,7 @@ var CONFIG_CHECKS = [
2832
2840
  filePattern: /next\.config\.(js|mjs|ts)$/,
2833
2841
  check(content) {
2834
2842
  if (!/poweredByHeader\s*:\s*false/.test(content)) {
2835
- return { line: 1, snippet: getSnippet4(content, 1) };
2843
+ return { line: 1, snippet: getSnippet5(content, 1) };
2836
2844
  }
2837
2845
  return null;
2838
2846
  }
@@ -2852,7 +2860,7 @@ var CONFIG_CHECKS = [
2852
2860
  const lines = content.split("\n");
2853
2861
  for (let i = 0; i < lines.length; i++) {
2854
2862
  if (/^FROM\s+\S+:latest/i.test(lines[i].trim()) || /^FROM\s+\w+\s*$/i.test(lines[i].trim())) {
2855
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2863
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2856
2864
  }
2857
2865
  }
2858
2866
  return null;
@@ -2871,7 +2879,7 @@ var CONFIG_CHECKS = [
2871
2879
  check(content) {
2872
2880
  const fromCount = (content.match(/^FROM\s/gmi) || []).length;
2873
2881
  if (fromCount <= 1 && content.includes("npm") && content.length > 200) {
2874
- return { line: 1, snippet: getSnippet4(content, 1) };
2882
+ return { line: 1, snippet: getSnippet5(content, 1) };
2875
2883
  }
2876
2884
  return null;
2877
2885
  }
@@ -2890,7 +2898,7 @@ var CONFIG_CHECKS = [
2890
2898
  const lines = content.split("\n");
2891
2899
  for (let i = 0; i < lines.length; i++) {
2892
2900
  if (/^COPY\s.*\.env\b/i.test(lines[i].trim())) {
2893
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2901
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2894
2902
  }
2895
2903
  }
2896
2904
  return null;
@@ -2911,7 +2919,7 @@ var CONFIG_CHECKS = [
2911
2919
  const lines = content.split("\n");
2912
2920
  for (let i = 0; i < lines.length; i++) {
2913
2921
  if (/permissions\s*:\s*write-all/i.test(lines[i])) {
2914
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2922
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2915
2923
  }
2916
2924
  }
2917
2925
  return null;
@@ -2931,7 +2939,7 @@ var CONFIG_CHECKS = [
2931
2939
  const lines = content.split("\n");
2932
2940
  for (let i = 0; i < lines.length; i++) {
2933
2941
  if (/uses:\s*\S+@(?:main|master|dev|latest)\s*$/i.test(lines[i])) {
2934
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2942
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2935
2943
  }
2936
2944
  }
2937
2945
  return null;
@@ -2952,7 +2960,7 @@ var CONFIG_CHECKS = [
2952
2960
  const lines = content.split("\n");
2953
2961
  for (let i = 0; i < lines.length; i++) {
2954
2962
  if (/host\s*:\s*true/.test(lines[i]) || /host\s*:\s*['"]0\.0\.0\.0['"]/.test(lines[i])) {
2955
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2963
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2956
2964
  }
2957
2965
  }
2958
2966
  return null;
@@ -2973,7 +2981,7 @@ var CONFIG_CHECKS = [
2973
2981
  const lines = content.split("\n");
2974
2982
  for (let i = 0; i < lines.length; i++) {
2975
2983
  if (/_authToken|_auth=|\/\/registry.*:_password/.test(lines[i])) {
2976
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2984
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2977
2985
  }
2978
2986
  }
2979
2987
  return null;
@@ -2994,7 +3002,7 @@ var CONFIG_CHECKS = [
2994
3002
  const lines = content.split("\n");
2995
3003
  for (let i = 0; i < lines.length; i++) {
2996
3004
  if (/no-eval|no-implied-eval|no-new-func|no-script-url|security\/detect/i.test(lines[i]) && /["']off["']|:\s*0/.test(lines[i])) {
2997
- return { line: i + 1, snippet: getSnippet4(content, i + 1) };
3005
+ return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2998
3006
  }
2999
3007
  }
3000
3008
  return null;
@@ -3030,7 +3038,7 @@ function scanConfigs(files) {
3030
3038
  }
3031
3039
 
3032
3040
  // src/scanners/multi-file-analyzer.ts
3033
- function getSnippet5(content, line) {
3041
+ function getSnippet6(content, line) {
3034
3042
  const lines = content.split("\n");
3035
3043
  const start = Math.max(0, line - 2);
3036
3044
  const end = Math.min(lines.length, line + 2);
@@ -3071,7 +3079,7 @@ function scanMultiFile(files) {
3071
3079
  description: "This API route file has no authentication checks and no auth middleware was detected in the project.",
3072
3080
  file: f.path,
3073
3081
  line,
3074
- snippet: getSnippet5(f.content, line),
3082
+ snippet: getSnippet6(f.content, line),
3075
3083
  fix: "Add authentication middleware or import your auth module. If using Next.js, create middleware.ts in your app root.",
3076
3084
  category: "Authentication",
3077
3085
  source: "custom",
@@ -3093,7 +3101,7 @@ function scanMultiFile(files) {
3093
3101
  description: "An .env file exists in the project but .gitignore doesn't exclude it. Secrets may be committed.",
3094
3102
  file: ".gitignore",
3095
3103
  line: 1,
3096
- snippet: getSnippet5(gitignore.content, 1),
3104
+ snippet: getSnippet6(gitignore.content, 1),
3097
3105
  fix: "Add .env to .gitignore: echo '.env' >> .gitignore",
3098
3106
  category: "Secrets",
3099
3107
  source: "custom",
@@ -3117,7 +3125,7 @@ function scanMultiFile(files) {
3117
3125
  description: "Creating a new PrismaClient on every request leaks database connections. Use a singleton pattern.",
3118
3126
  file: f.path,
3119
3127
  line,
3120
- snippet: getSnippet5(f.content, line),
3128
+ snippet: getSnippet6(f.content, line),
3121
3129
  fix: "Use singleton: globalThis.prisma = globalThis.prisma || new PrismaClient(). This prevents connection exhaustion in serverless.",
3122
3130
  category: "Configuration",
3123
3131
  source: "custom",
@@ -3142,7 +3150,7 @@ function scanMultiFile(files) {
3142
3150
  description: "CORS origin is hardcoded to localhost. This will block requests from your production frontend.",
3143
3151
  file: f.path,
3144
3152
  line,
3145
- snippet: getSnippet5(f.content, line),
3153
+ snippet: getSnippet6(f.content, line),
3146
3154
  fix: "Use environment variable: origin: process.env.FRONTEND_URL || 'http://localhost:3000'",
3147
3155
  category: "Configuration",
3148
3156
  source: "custom",
@@ -3178,7 +3186,7 @@ function scanMultiFile(files) {
3178
3186
  description: `These environment variables are used in code but missing from .env.example: ${undocumented.slice(0, 5).join(", ")}${undocumented.length > 5 ? ` (+${undocumented.length - 5} more)` : ""}`,
3179
3187
  file: envExample.path,
3180
3188
  line: 1,
3181
- snippet: getSnippet5(envExample.content, 1),
3189
+ snippet: getSnippet6(envExample.content, 1),
3182
3190
  fix: "Add missing variables to .env.example so team members know which env vars are needed.",
3183
3191
  category: "Configuration",
3184
3192
  source: "custom",
@@ -3201,7 +3209,7 @@ function scanMultiFile(files) {
3201
3209
  description: `Fetching from ${m[1].substring(0, 40)}... over HTTP exposes data to interception.`,
3202
3210
  file: f.path,
3203
3211
  line,
3204
- snippet: getSnippet5(f.content, line),
3212
+ snippet: getSnippet6(f.content, line),
3205
3213
  fix: "Use HTTPS for all API calls in production.",
3206
3214
  category: "Configuration",
3207
3215
  source: "custom",