xploitscan 1.0.16 → 1.0.18
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 +21 -0
- package/dist/api-HTHCG6QE.js +0 -0
- package/dist/chunk-RJUUWD2F.js +0 -0
- package/dist/index.js +177 -152
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
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.
|
package/dist/api-HTHCG6QE.js
CHANGED
|
File without changes
|
package/dist/chunk-RJUUWD2F.js
CHANGED
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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",
|
|
@@ -4102,7 +4127,7 @@ async function cursorInstallCommand(opts = {}) {
|
|
|
4102
4127
|
var program = new Command();
|
|
4103
4128
|
program.name("xploitscan").description(
|
|
4104
4129
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
4105
|
-
).version("1.0.
|
|
4130
|
+
).version("1.0.18");
|
|
4106
4131
|
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option("-f, --format <format>", "Output format: terminal, json, sarif", "terminal").option("-v, --verbose", "Show detailed output", false).option("--diff [base]", "Scan only files changed vs base branch (default: main)").option("-w, --watch", "Watch for file changes and re-scan automatically", false).action(async (directory, opts) => {
|
|
4107
4132
|
await scanCommand(directory, {
|
|
4108
4133
|
directory,
|