xploitscan 1.0.10 → 1.0.11
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/README.md +3 -3
- package/dist/index.js +153 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
**Security scanner for AI-generated code.** Find vulnerabilities before attackers do.
|
|
7
7
|
|
|
8
|
-
Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code.
|
|
8
|
+
Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 158 security rules. Plain-English results. Copy-paste fixes.
|
|
9
9
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ No install, no config, no account required. Your code stays 100% local.
|
|
|
17
17
|
|
|
18
18
|
## What It Catches
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
158 rules across 15+ categories:
|
|
21
21
|
|
|
22
22
|
| Category | Examples | Rules |
|
|
23
23
|
|----------|---------|-------|
|
|
@@ -130,7 +130,7 @@ Scan via the web at [xploitscan.com](https://xploitscan.com):
|
|
|
130
130
|
- SOC2/ISO27001 compliance mapping
|
|
131
131
|
- Slack and Discord webhook notifications
|
|
132
132
|
|
|
133
|
-
**Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all
|
|
133
|
+
**Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all 158 rules, and all dashboard features. **Team** ($99/mo): everything in Pro plus 5 seats, shared scan history, RBAC, and portfolio reports. Annual plans save 20%.
|
|
134
134
|
|
|
135
135
|
## Supported Languages
|
|
136
136
|
|
package/dist/index.js
CHANGED
|
@@ -1170,6 +1170,7 @@ var dangerousInnerHTML = {
|
|
|
1170
1170
|
const value = m[1].trim();
|
|
1171
1171
|
if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
|
|
1172
1172
|
if (/^["'`][^$]*["'`]$/.test(value)) continue;
|
|
1173
|
+
if (/JSON\.stringify/i.test(value)) continue;
|
|
1173
1174
|
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
1174
1175
|
findings.push({
|
|
1175
1176
|
rule: "VC063",
|
|
@@ -1403,7 +1404,15 @@ var complianceMap = {
|
|
|
1403
1404
|
VC148: { owasp: "A09:2021", cwe: "CWE-209" },
|
|
1404
1405
|
VC149: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1405
1406
|
VC150: { owasp: "A07:2021", cwe: "CWE-615" },
|
|
1406
|
-
VC151: { owasp: "A07:2021", cwe: "CWE-214" }
|
|
1407
|
+
VC151: { owasp: "A07:2021", cwe: "CWE-214" },
|
|
1408
|
+
// VC152–VC158: New rules
|
|
1409
|
+
VC152: { owasp: "A08:2021", cwe: "CWE-345" },
|
|
1410
|
+
VC153: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
1411
|
+
VC154: { owasp: "A03:2021", cwe: "CWE-20" },
|
|
1412
|
+
VC155: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
1413
|
+
VC156: { owasp: "A04:2021", cwe: "CWE-770" },
|
|
1414
|
+
VC157: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
1415
|
+
VC158: { owasp: "A01:2021", cwe: "CWE-639" }
|
|
1407
1416
|
};
|
|
1408
1417
|
var consoleLogProduction = {
|
|
1409
1418
|
id: "VC097",
|
|
@@ -1529,6 +1538,8 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
|
|
|
1529
1538
|
if (/function runScan\(files\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\s*[:=]/.test(content) && /findMatches/.test(content)) {
|
|
1530
1539
|
return findings;
|
|
1531
1540
|
}
|
|
1541
|
+
if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
|
|
1542
|
+
if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
|
|
1532
1543
|
const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
|
|
1533
1544
|
for (const rule of ruleset) {
|
|
1534
1545
|
if (disabledRules.includes(rule.id)) continue;
|
|
@@ -1905,6 +1916,120 @@ function chunkFiles(files, maxChars) {
|
|
|
1905
1916
|
return chunks;
|
|
1906
1917
|
}
|
|
1907
1918
|
|
|
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
|
+
if (!process.env.ANTHROPIC_API_KEY) return findings;
|
|
1987
|
+
if (findings.length === 0) return findings;
|
|
1988
|
+
const toReview = findings.slice(0, MAX_TOTAL_FINDINGS);
|
|
1989
|
+
const overflow = findings.slice(MAX_TOTAL_FINDINGS);
|
|
1990
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1991
|
+
for (const f of toReview) {
|
|
1992
|
+
const group = byFile.get(f.file) || [];
|
|
1993
|
+
group.push(f);
|
|
1994
|
+
byFile.set(f.file, group);
|
|
1995
|
+
}
|
|
1996
|
+
let client;
|
|
1997
|
+
try {
|
|
1998
|
+
client = new Anthropic2();
|
|
1999
|
+
} catch {
|
|
2000
|
+
return findings;
|
|
2001
|
+
}
|
|
2002
|
+
const fpIndices = /* @__PURE__ */ new Set();
|
|
2003
|
+
for (const [file, fileFindings] of byFile) {
|
|
2004
|
+
const content = fileContents.get(file);
|
|
2005
|
+
if (!content) continue;
|
|
2006
|
+
for (let i = 0; i < fileFindings.length; i += MAX_FINDINGS_PER_BATCH) {
|
|
2007
|
+
const batch = fileFindings.slice(i, i + MAX_FINDINGS_PER_BATCH);
|
|
2008
|
+
const prompt = buildReviewPrompt(batch, content);
|
|
2009
|
+
try {
|
|
2010
|
+
const response = await client.messages.create({
|
|
2011
|
+
model: "claude-haiku-4-5-20251001",
|
|
2012
|
+
max_tokens: 1024,
|
|
2013
|
+
system: REVIEW_SYSTEM_PROMPT,
|
|
2014
|
+
messages: [{ role: "user", content: prompt }]
|
|
2015
|
+
});
|
|
2016
|
+
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
2017
|
+
const results = parseReviewResponse(text);
|
|
2018
|
+
for (const r of results) {
|
|
2019
|
+
if (r.verdict === "fp" && r.index >= 0 && r.index < batch.length) {
|
|
2020
|
+
const globalIndex = toReview.indexOf(batch[r.index]);
|
|
2021
|
+
if (globalIndex !== -1) fpIndices.add(globalIndex);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
} catch {
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
const filtered = toReview.filter((_, i) => !fpIndices.has(i));
|
|
2030
|
+
return [...filtered, ...overflow];
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1908
2033
|
// src/scanners/ast-analyzer.ts
|
|
1909
2034
|
function stripComments(content) {
|
|
1910
2035
|
let result = "";
|
|
@@ -2557,6 +2682,7 @@ function scanEntropy(files) {
|
|
|
2557
2682
|
if (SKIP_FILENAMES.test(basename)) continue;
|
|
2558
2683
|
if (filePath.includes("node_modules")) continue;
|
|
2559
2684
|
if (filePath.includes(".min.")) continue;
|
|
2685
|
+
if (/pro-rules-bundle|\.bundle\.|\.chunk\./i.test(filePath)) continue;
|
|
2560
2686
|
if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
|
|
2561
2687
|
const lines = content.split("\n");
|
|
2562
2688
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3500,6 +3626,30 @@ async function scanCommand(directory, options) {
|
|
|
3500
3626
|
chalk2.gray("Tip: Set ANTHROPIC_API_KEY for AI-powered contextual analysis")
|
|
3501
3627
|
);
|
|
3502
3628
|
}
|
|
3629
|
+
if (process.env.ANTHROPIC_API_KEY && allFindings.length > 0) {
|
|
3630
|
+
spinner.text = "AI reviewing findings for false positives...";
|
|
3631
|
+
spinner.color = "cyan";
|
|
3632
|
+
try {
|
|
3633
|
+
const fileContentsMap = /* @__PURE__ */ new Map();
|
|
3634
|
+
for (const f of allFindings) {
|
|
3635
|
+
if (!fileContentsMap.has(f.file)) {
|
|
3636
|
+
const content = readFileContents(dir, f.file);
|
|
3637
|
+
if (content) fileContentsMap.set(f.file, content);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
const beforeCount = allFindings.length;
|
|
3641
|
+
const filtered = await filterFalsePositives(allFindings, fileContentsMap);
|
|
3642
|
+
const removed = beforeCount - filtered.length;
|
|
3643
|
+
if (removed > 0) {
|
|
3644
|
+
allFindings.length = 0;
|
|
3645
|
+
allFindings.push(...filtered);
|
|
3646
|
+
}
|
|
3647
|
+
if (removed > 0 && verbose) {
|
|
3648
|
+
spinner.info(`AI review removed ${removed} false positive${removed !== 1 ? "s" : ""}`);
|
|
3649
|
+
}
|
|
3650
|
+
} catch {
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3503
3653
|
spinner.stop();
|
|
3504
3654
|
const seen = /* @__PURE__ */ new Set();
|
|
3505
3655
|
const dedupedFindings = allFindings.filter((f) => {
|
|
@@ -3529,9 +3679,9 @@ async function scanCommand(directory, options) {
|
|
|
3529
3679
|
if (tier === "free" && !isSilent) {
|
|
3530
3680
|
console.log("");
|
|
3531
3681
|
if (userPlan === "anonymous") {
|
|
3532
|
-
console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all
|
|
3682
|
+
console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 158 rules \u2192") + chalk2.bold(" xploitscan auth login"));
|
|
3533
3683
|
} else {
|
|
3534
|
-
console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all
|
|
3684
|
+
console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 158 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
|
|
3535
3685
|
}
|
|
3536
3686
|
console.log("");
|
|
3537
3687
|
}
|