xploitscan 1.0.16 → 1.0.17

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeCheck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
File without changes
File without changes
package/dist/index.js CHANGED
@@ -1702,6 +1702,161 @@ async function filterFalsePositives(findings, fileContents) {
1702
1702
  totalBefore
1703
1703
  };
1704
1704
  }
1705
+ function shannonEntropy(str) {
1706
+ const freq = {};
1707
+ for (const ch of str) {
1708
+ freq[ch] = (freq[ch] || 0) + 1;
1709
+ }
1710
+ const len = str.length;
1711
+ let entropy = 0;
1712
+ for (const count of Object.values(freq)) {
1713
+ const p = count / len;
1714
+ entropy -= p * Math.log2(p);
1715
+ }
1716
+ return entropy;
1717
+ }
1718
+ var SAFE_PATTERNS = [
1719
+ // UUIDs (v1-v5)
1720
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
1721
+ // Git commit hashes (long and short)
1722
+ /^[0-9a-f]{40}$/i,
1723
+ /^[0-9a-f]{7,8}$/i,
1724
+ // Hex colors
1725
+ /^#?[0-9a-fA-F]{3,8}$/,
1726
+ // Small base64 (< 20 chars can't be a real key anyway)
1727
+ /^[A-Za-z0-9+/]{1,19}={0,2}$/,
1728
+ // Package versions
1729
+ /^\d+\.\d+\.\d+/,
1730
+ // Integrity-hash prefix (sha256-, sha512-, ...)
1731
+ /^sha\d+-/i,
1732
+ // URLs without credentials in them
1733
+ /^https?:\/\/[^:@]*$/,
1734
+ /^https?:\/\/registry\.npmjs\.org\//,
1735
+ /^https?:\/\/registry\.yarnpkg\.com\//,
1736
+ // Full package integrity hash (sha512-[base64]=)
1737
+ /^sha\d+-[A-Za-z0-9+/=]+$/,
1738
+ // Package tarball URLs
1739
+ /\.tgz$/,
1740
+ /registry.*\/-\/.*\.tgz$/,
1741
+ // ISO dates / times
1742
+ /^\d{4}-\d{2}-\d{2}/,
1743
+ // Locale tags
1744
+ /^[a-z]{2}-[A-Z]{2}$/,
1745
+ // Text encodings
1746
+ /^utf-?8|ascii|latin|iso-8859/i,
1747
+ // MIME types
1748
+ /^(?:application|text|image|audio|video)\//,
1749
+ // CSS keyword values
1750
+ /^(?:inherit|none|auto|block|flex|grid|absolute|relative|fixed|px|em|rem|%)/,
1751
+ // Developer-placeholder tokens
1752
+ /^(?:test|example|sample|demo|placeholder|temp|tmp|foo|bar|baz|lorem|ipsum)/i,
1753
+ // XML / DTD markers
1754
+ /DTD|DOCTYPE|w3\.org|apple\.com\/DTDs/i,
1755
+ /xmlns|schema|xsd|xsi/i,
1756
+ // NEW — added in Wave 3.2 for context-aware FP reduction
1757
+ // ──────────────────────────────────────────────────────
1758
+ // Tailwind JIT / CSS-in-JS class-name fingerprints, e.g. "css-2kx3yr8",
1759
+ // "tw-abc12def", "jss-1a2b3c4d". Typically prefix + 6-12 hex-ish chars.
1760
+ /^(?:css|tw|jss|emotion|styled|mui|chakra)-[a-z0-9]{4,14}$/i,
1761
+ // SVG path data — starts with a path command letter followed by coords.
1762
+ // These can get very long and high-entropy but are never secrets.
1763
+ /^[MmLlHhVvCcSsQqTtAaZz][\d.,\s\-MmLlHhVvCcSsQqTtAaZz]{10,}$/,
1764
+ // Next.js / Vite / webpack content-addressed asset filenames, e.g.
1765
+ // "main.4e5f6a78.js", "chunk-2a3b.js", "_next/static/chunks/pages-xyz".
1766
+ /\.[0-9a-f]{6,16}\.(?:js|css|mjs|woff2?|ttf|png|jpg|svg)(?:\?.*)?$/i,
1767
+ /^_next\//,
1768
+ // Publishable / client-side keys from common vendors. Flagged by their
1769
+ // own service-specific rules if they look wrong, but entropy should NOT
1770
+ // double-flag these. They are designed to ship to the browser.
1771
+ /^pk_(?:live|test|[a-z0-9]+)_/,
1772
+ // Stripe / Clerk publishable
1773
+ /^NEXT_PUBLIC_|^VITE_|^REACT_APP_/,
1774
+ // Build-time public env vars
1775
+ /^pub_/,
1776
+ // Segment etc.
1777
+ /^ey[A-Za-z0-9_-]+\.ey[A-Za-z0-9_-]+\./,
1778
+ // JWT header.payload prefix — don't flag solely on entropy
1779
+ // Content-Security-Policy hashes / nonces in HTML/JSON
1780
+ /^'sha\d+-/,
1781
+ /^nonce-/i,
1782
+ // Embedded data URIs
1783
+ /^data:[a-z]+\//i
1784
+ ];
1785
+ 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;
1786
+ var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
1787
+ var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class(?:Name)?|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset|locale|translation|copy|prose|markdown|slug|handle)/i;
1788
+ var HASH_LIKE_VAR_NAMES = /(?:^|[^a-z])(?:hash|digest|checksum|fingerprint|etag|crc|md5|sha1|sha256|sha512|contenthash|buildid|revision|commit|sri|integrity|cacheKey|fileHash|assetId|versionId)(?:$|[^a-z])/i;
1789
+ var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
1790
+ var SECRET_VAR_NAMES = /(?:^|[^a-z])(?:key|secret|token|password|passwd|pwd|api[_-]?key|auth|credential|private|signing|bearer|access[_-]?token|refresh[_-]?token|session[_-]?id)(?:$|[^a-z])/i;
1791
+ function getSnippet22(content, line) {
1792
+ const lines = content.split("\n");
1793
+ const start = Math.max(0, line - 2);
1794
+ const end = Math.min(lines.length, line + 2);
1795
+ return lines.slice(start, end).map((l, i) => {
1796
+ const lineNum = start + i + 1;
1797
+ const prefix = lineNum === line ? ">" : " ";
1798
+ return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;
1799
+ }).join("\n");
1800
+ }
1801
+ function scanEntropy(files) {
1802
+ const findings = [];
1803
+ for (const { path: filePath, content } of files) {
1804
+ if (SKIP_FILES.test(filePath)) continue;
1805
+ if (SKIP_FILENAMES.test(filePath)) continue;
1806
+ const basename = filePath.split("/").pop() || "";
1807
+ if (SKIP_FILENAMES.test(basename)) continue;
1808
+ if (filePath.includes("node_modules")) continue;
1809
+ if (filePath.includes(".min.")) continue;
1810
+ if (/pro-rules-bundle|\.bundle\.|\.chunk\./i.test(filePath)) continue;
1811
+ if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
1812
+ const lines = content.split("\n");
1813
+ for (let i = 0; i < lines.length; i++) {
1814
+ const line = lines[i];
1815
+ const trimmed = line.trimStart();
1816
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
1817
+ const stringPattern = /(?:[:=]\s*)(["'`])([^"'`\n]{20,120})\1/g;
1818
+ let match;
1819
+ while ((match = stringPattern.exec(line)) !== null) {
1820
+ const value = match[2];
1821
+ if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
1822
+ if (HASH_PREFIX_RE.test(value)) continue;
1823
+ const beforeAssign = line.substring(0, match.index);
1824
+ if (SAFE_VAR_NAMES.test(beforeAssign)) continue;
1825
+ if (/^https?:\/\/[^:@]*$/.test(value)) continue;
1826
+ if ((value.match(/\s/g) || []).length > 2) continue;
1827
+ const isHex = /^[0-9a-fA-F]+$/.test(value);
1828
+ const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);
1829
+ let threshold = 4;
1830
+ if (isHex) threshold = 3;
1831
+ else if (isBase64) threshold = 4.5;
1832
+ if (value.length < 20) continue;
1833
+ const entropy = shannonEntropy(value);
1834
+ if (entropy < threshold) continue;
1835
+ const varName = beforeAssign.match(/(\w+)\s*[:=]\s*$/)?.[1] || "";
1836
+ if (HASH_LIKE_VAR_NAMES.test(varName) && (isHex || isBase64)) continue;
1837
+ const isLikelySecret = SECRET_VAR_NAMES.test(varName);
1838
+ if (entropy < 4.5 && !isLikelySecret) continue;
1839
+ const masked = value.substring(0, 6) + "..." + value.substring(value.length - 4);
1840
+ findings.push({
1841
+ id: `ENTROPY-${filePath}:${i + 1}`,
1842
+ rule: "ENTROPY",
1843
+ severity: isLikelySecret ? "critical" : "high",
1844
+ title: "High-Entropy String Detected (Possible Secret)",
1845
+ description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: "${masked}"`,
1846
+ file: filePath,
1847
+ line: i + 1,
1848
+ snippet: getSnippet22(content, i + 1),
1849
+ 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.",
1850
+ category: "Secrets",
1851
+ source: "custom",
1852
+ owasp: "A02:2021",
1853
+ cwe: "CWE-798"
1854
+ });
1855
+ }
1856
+ }
1857
+ }
1858
+ return findings;
1859
+ }
1705
1860
 
1706
1861
  // src/scanners/semgrep.ts
1707
1862
  import { execFile } from "child_process";
@@ -2625,138 +2780,8 @@ async function scanDependenciesOsv(files, alreadyFoundByRule) {
2625
2780
  return findings;
2626
2781
  }
2627
2782
 
2628
- // src/scanners/entropy-scanner.ts
2629
- function shannonEntropy(str) {
2630
- const freq = {};
2631
- for (const ch of str) {
2632
- freq[ch] = (freq[ch] || 0) + 1;
2633
- }
2634
- const len = str.length;
2635
- let entropy = 0;
2636
- for (const count of Object.values(freq)) {
2637
- const p = count / len;
2638
- entropy -= p * Math.log2(p);
2639
- }
2640
- return entropy;
2641
- }
2642
- var SAFE_PATTERNS = [
2643
- // UUIDs
2644
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
2645
- // Git commit hashes
2646
- /^[0-9a-f]{40}$/i,
2647
- // Short git hashes
2648
- /^[0-9a-f]{7,8}$/i,
2649
- // Hex colors
2650
- /^#?[0-9a-fA-F]{3,8}$/,
2651
- // Base64 encoded small data (< 20 chars)
2652
- /^[A-Za-z0-9+/]{1,19}={0,2}$/,
2653
- // Package versions
2654
- /^\d+\.\d+\.\d+/,
2655
- // File hashes (integrity)
2656
- /^sha\d+-/i,
2657
- // URLs without credentials
2658
- /^https?:\/\/[^:@]*$/,
2659
- // Date/time strings
2660
- /^\d{4}-\d{2}-\d{2}/,
2661
- // Locale strings
2662
- /^[a-z]{2}-[A-Z]{2}$/,
2663
- // Common encodings
2664
- /^utf-?8|ascii|latin|iso-8859/i,
2665
- // MIME types
2666
- /^(?:application|text|image|audio|video)\//,
2667
- // CSS/HTML values
2668
- /^(?:inherit|none|auto|block|flex|grid|absolute|relative|fixed|px|em|rem|%)/,
2669
- // Common placeholder/test values
2670
- /^(?:test|example|sample|demo|placeholder|temp|tmp|foo|bar|baz|lorem|ipsum)/i,
2671
- // DOCTYPE/DTD URLs
2672
- /DTD|DOCTYPE|w3\.org|apple\.com\/DTDs/i,
2673
- // XML namespaces
2674
- /xmlns|schema|xsd|xsi/i,
2675
- // npm/package registry URLs
2676
- /^https?:\/\/registry\.npmjs\.org\//,
2677
- /^https?:\/\/registry\.yarnpkg\.com\//,
2678
- // Package integrity hashes (sha512-..., sha256-...)
2679
- /^sha\d+-[A-Za-z0-9+/=]+$/,
2680
- // Resolved package URLs (.tgz)
2681
- /\.tgz$/,
2682
- // npm resolved URLs (any registry URL with package tarball)
2683
- /registry.*\/-\/.*\.tgz$/
2684
- ];
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;
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;
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;
2688
- function getSnippet4(content, line) {
2689
- const lines = content.split("\n");
2690
- const start = Math.max(0, line - 2);
2691
- const end = Math.min(lines.length, line + 2);
2692
- return lines.slice(start, end).map((l, i) => {
2693
- const lineNum = start + i + 1;
2694
- const prefix = lineNum === line ? ">" : " ";
2695
- return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;
2696
- }).join("\n");
2697
- }
2698
- function scanEntropy(files) {
2699
- const findings = [];
2700
- for (const { path: filePath, content } of files) {
2701
- if (SKIP_FILES.test(filePath)) continue;
2702
- if (SKIP_FILENAMES.test(filePath)) continue;
2703
- const basename = filePath.split("/").pop() || "";
2704
- if (SKIP_FILENAMES.test(basename)) continue;
2705
- if (filePath.includes("node_modules")) continue;
2706
- if (filePath.includes(".min.")) continue;
2707
- if (/pro-rules-bundle|\.bundle\.|\.chunk\./i.test(filePath)) continue;
2708
- if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
2709
- const lines = content.split("\n");
2710
- for (let i = 0; i < lines.length; i++) {
2711
- const line = lines[i];
2712
- const trimmed = line.trimStart();
2713
- if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
2714
- const stringPattern = /(?:[:=]\s*)(["'`])([^"'`\n]{20,120})\1/g;
2715
- let match;
2716
- while ((match = stringPattern.exec(line)) !== null) {
2717
- const value = match[2];
2718
- if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
2719
- const beforeAssign = line.substring(0, match.index);
2720
- if (SAFE_VAR_NAMES.test(beforeAssign)) continue;
2721
- if (/^https?:\/\/[^:@]*$/.test(value)) continue;
2722
- if ((value.match(/\s/g) || []).length > 2) continue;
2723
- const entropy = shannonEntropy(value);
2724
- const isHex = /^[0-9a-fA-F]+$/.test(value);
2725
- const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);
2726
- let threshold = 4;
2727
- if (isHex) threshold = 3;
2728
- else if (isBase64) threshold = 4.5;
2729
- if (value.length < 20) continue;
2730
- if (entropy >= threshold) {
2731
- const varName = beforeAssign.match(/(\w+)\s*[:=]\s*$/)?.[1] || "";
2732
- const isLikelySecret = /(?:key|secret|token|password|passwd|pwd|api_?key|auth|credential|private|signing)/i.test(varName);
2733
- if (entropy >= 4.5 || isLikelySecret) {
2734
- const masked = value.substring(0, 6) + "..." + value.substring(value.length - 4);
2735
- findings.push({
2736
- id: `ENTROPY-${filePath}:${i + 1}`,
2737
- rule: "ENTROPY",
2738
- severity: isLikelySecret ? "critical" : "high",
2739
- title: "High-Entropy String Detected (Possible Secret)",
2740
- description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: "${masked}"`,
2741
- file: filePath,
2742
- line: i + 1,
2743
- snippet: getSnippet4(content, i + 1),
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.",
2745
- category: "Secrets",
2746
- source: "custom",
2747
- owasp: "A02:2021",
2748
- cwe: "CWE-798"
2749
- });
2750
- }
2751
- }
2752
- }
2753
- }
2754
- }
2755
- return findings;
2756
- }
2757
-
2758
2783
  // src/scanners/config-analyzer.ts
2759
- function getSnippet5(content, line) {
2784
+ function getSnippet4(content, line) {
2760
2785
  const lines = content.split("\n");
2761
2786
  const start = Math.max(0, line - 2);
2762
2787
  const end = Math.min(lines.length, line + 2);
@@ -2782,10 +2807,10 @@ var CONFIG_CHECKS = [
2782
2807
  try {
2783
2808
  if (/"strict"\s*:\s*false/.test(content)) {
2784
2809
  const line = content.split("\n").findIndex((l) => /strict/.test(l)) + 1;
2785
- return { line, snippet: getSnippet5(content, line) };
2810
+ return { line, snippet: getSnippet4(content, line) };
2786
2811
  }
2787
2812
  if (!/"strict"\s*:\s*true/.test(content) && /"compilerOptions"/.test(content)) {
2788
- return { line: 1, snippet: getSnippet5(content, 1) };
2813
+ return { line: 1, snippet: getSnippet4(content, 1) };
2789
2814
  }
2790
2815
  } catch {
2791
2816
  }
@@ -2805,7 +2830,7 @@ var CONFIG_CHECKS = [
2805
2830
  check(content) {
2806
2831
  if (/"allowJs"\s*:\s*true/.test(content) && !/"checkJs"\s*:\s*true/.test(content)) {
2807
2832
  const line = content.split("\n").findIndex((l) => /allowJs/.test(l)) + 1;
2808
- return { line, snippet: getSnippet5(content, line) };
2833
+ return { line, snippet: getSnippet4(content, line) };
2809
2834
  }
2810
2835
  return null;
2811
2836
  }
@@ -2823,7 +2848,7 @@ var CONFIG_CHECKS = [
2823
2848
  filePattern: /next\.config\.(js|mjs|ts)$/,
2824
2849
  check(content) {
2825
2850
  if (!/headers\s*\(/.test(content) && !/securityHeaders|security-headers/.test(content)) {
2826
- return { line: 1, snippet: getSnippet5(content, 1) };
2851
+ return { line: 1, snippet: getSnippet4(content, 1) };
2827
2852
  }
2828
2853
  return null;
2829
2854
  }
@@ -2840,7 +2865,7 @@ var CONFIG_CHECKS = [
2840
2865
  filePattern: /next\.config\.(js|mjs|ts)$/,
2841
2866
  check(content) {
2842
2867
  if (!/poweredByHeader\s*:\s*false/.test(content)) {
2843
- return { line: 1, snippet: getSnippet5(content, 1) };
2868
+ return { line: 1, snippet: getSnippet4(content, 1) };
2844
2869
  }
2845
2870
  return null;
2846
2871
  }
@@ -2860,7 +2885,7 @@ var CONFIG_CHECKS = [
2860
2885
  const lines = content.split("\n");
2861
2886
  for (let i = 0; i < lines.length; i++) {
2862
2887
  if (/^FROM\s+\S+:latest/i.test(lines[i].trim()) || /^FROM\s+\w+\s*$/i.test(lines[i].trim())) {
2863
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2888
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2864
2889
  }
2865
2890
  }
2866
2891
  return null;
@@ -2879,7 +2904,7 @@ var CONFIG_CHECKS = [
2879
2904
  check(content) {
2880
2905
  const fromCount = (content.match(/^FROM\s/gmi) || []).length;
2881
2906
  if (fromCount <= 1 && content.includes("npm") && content.length > 200) {
2882
- return { line: 1, snippet: getSnippet5(content, 1) };
2907
+ return { line: 1, snippet: getSnippet4(content, 1) };
2883
2908
  }
2884
2909
  return null;
2885
2910
  }
@@ -2898,7 +2923,7 @@ var CONFIG_CHECKS = [
2898
2923
  const lines = content.split("\n");
2899
2924
  for (let i = 0; i < lines.length; i++) {
2900
2925
  if (/^COPY\s.*\.env\b/i.test(lines[i].trim())) {
2901
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2926
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2902
2927
  }
2903
2928
  }
2904
2929
  return null;
@@ -2919,7 +2944,7 @@ var CONFIG_CHECKS = [
2919
2944
  const lines = content.split("\n");
2920
2945
  for (let i = 0; i < lines.length; i++) {
2921
2946
  if (/permissions\s*:\s*write-all/i.test(lines[i])) {
2922
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2947
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2923
2948
  }
2924
2949
  }
2925
2950
  return null;
@@ -2939,7 +2964,7 @@ var CONFIG_CHECKS = [
2939
2964
  const lines = content.split("\n");
2940
2965
  for (let i = 0; i < lines.length; i++) {
2941
2966
  if (/uses:\s*\S+@(?:main|master|dev|latest)\s*$/i.test(lines[i])) {
2942
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2967
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2943
2968
  }
2944
2969
  }
2945
2970
  return null;
@@ -2960,7 +2985,7 @@ var CONFIG_CHECKS = [
2960
2985
  const lines = content.split("\n");
2961
2986
  for (let i = 0; i < lines.length; i++) {
2962
2987
  if (/host\s*:\s*true/.test(lines[i]) || /host\s*:\s*['"]0\.0\.0\.0['"]/.test(lines[i])) {
2963
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
2988
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2964
2989
  }
2965
2990
  }
2966
2991
  return null;
@@ -2981,7 +3006,7 @@ var CONFIG_CHECKS = [
2981
3006
  const lines = content.split("\n");
2982
3007
  for (let i = 0; i < lines.length; i++) {
2983
3008
  if (/_authToken|_auth=|\/\/registry.*:_password/.test(lines[i])) {
2984
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
3009
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
2985
3010
  }
2986
3011
  }
2987
3012
  return null;
@@ -3002,7 +3027,7 @@ var CONFIG_CHECKS = [
3002
3027
  const lines = content.split("\n");
3003
3028
  for (let i = 0; i < lines.length; i++) {
3004
3029
  if (/no-eval|no-implied-eval|no-new-func|no-script-url|security\/detect/i.test(lines[i]) && /["']off["']|:\s*0/.test(lines[i])) {
3005
- return { line: i + 1, snippet: getSnippet5(content, i + 1) };
3030
+ return { line: i + 1, snippet: getSnippet4(content, i + 1) };
3006
3031
  }
3007
3032
  }
3008
3033
  return null;
@@ -3038,7 +3063,7 @@ function scanConfigs(files) {
3038
3063
  }
3039
3064
 
3040
3065
  // src/scanners/multi-file-analyzer.ts
3041
- function getSnippet6(content, line) {
3066
+ function getSnippet5(content, line) {
3042
3067
  const lines = content.split("\n");
3043
3068
  const start = Math.max(0, line - 2);
3044
3069
  const end = Math.min(lines.length, line + 2);
@@ -3079,7 +3104,7 @@ function scanMultiFile(files) {
3079
3104
  description: "This API route file has no authentication checks and no auth middleware was detected in the project.",
3080
3105
  file: f.path,
3081
3106
  line,
3082
- snippet: getSnippet6(f.content, line),
3107
+ snippet: getSnippet5(f.content, line),
3083
3108
  fix: "Add authentication middleware or import your auth module. If using Next.js, create middleware.ts in your app root.",
3084
3109
  category: "Authentication",
3085
3110
  source: "custom",
@@ -3101,7 +3126,7 @@ function scanMultiFile(files) {
3101
3126
  description: "An .env file exists in the project but .gitignore doesn't exclude it. Secrets may be committed.",
3102
3127
  file: ".gitignore",
3103
3128
  line: 1,
3104
- snippet: getSnippet6(gitignore.content, 1),
3129
+ snippet: getSnippet5(gitignore.content, 1),
3105
3130
  fix: "Add .env to .gitignore: echo '.env' >> .gitignore",
3106
3131
  category: "Secrets",
3107
3132
  source: "custom",
@@ -3125,7 +3150,7 @@ function scanMultiFile(files) {
3125
3150
  description: "Creating a new PrismaClient on every request leaks database connections. Use a singleton pattern.",
3126
3151
  file: f.path,
3127
3152
  line,
3128
- snippet: getSnippet6(f.content, line),
3153
+ snippet: getSnippet5(f.content, line),
3129
3154
  fix: "Use singleton: globalThis.prisma = globalThis.prisma || new PrismaClient(). This prevents connection exhaustion in serverless.",
3130
3155
  category: "Configuration",
3131
3156
  source: "custom",
@@ -3150,7 +3175,7 @@ function scanMultiFile(files) {
3150
3175
  description: "CORS origin is hardcoded to localhost. This will block requests from your production frontend.",
3151
3176
  file: f.path,
3152
3177
  line,
3153
- snippet: getSnippet6(f.content, line),
3178
+ snippet: getSnippet5(f.content, line),
3154
3179
  fix: "Use environment variable: origin: process.env.FRONTEND_URL || 'http://localhost:3000'",
3155
3180
  category: "Configuration",
3156
3181
  source: "custom",
@@ -3186,7 +3211,7 @@ function scanMultiFile(files) {
3186
3211
  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)` : ""}`,
3187
3212
  file: envExample.path,
3188
3213
  line: 1,
3189
- snippet: getSnippet6(envExample.content, 1),
3214
+ snippet: getSnippet5(envExample.content, 1),
3190
3215
  fix: "Add missing variables to .env.example so team members know which env vars are needed.",
3191
3216
  category: "Configuration",
3192
3217
  source: "custom",
@@ -3209,7 +3234,7 @@ function scanMultiFile(files) {
3209
3234
  description: `Fetching from ${m[1].substring(0, 40)}... over HTTP exposes data to interception.`,
3210
3235
  file: f.path,
3211
3236
  line,
3212
- snippet: getSnippet6(f.content, line),
3237
+ snippet: getSnippet5(f.content, line),
3213
3238
  fix: "Use HTTPS for all API calls in production.",
3214
3239
  category: "Configuration",
3215
3240
  source: "custom",