xploitscan 1.0.15 → 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 +175 -167
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -270,7 +270,18 @@ async function loadConfig(directory) {
|
|
|
270
270
|
return config;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
//
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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",
|