xploitscan 1.0.10 → 1.0.12

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 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. 151 security rules. Plain-English results. Copy-paste fixes.
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
- 151 rules across 15+ categories:
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 151 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%.
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,121 @@ 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, aiReviewed: false, removedCount: 0 };
1987
+ if (findings.length === 0) return { findings, aiReviewed: false, removedCount: 0 };
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, aiReviewed: false, removedCount: 0 };
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
+ const result = [...filtered, ...overflow];
2031
+ return { findings: result, aiReviewed: true, removedCount: fpIndices.size };
2032
+ }
2033
+
1908
2034
  // src/scanners/ast-analyzer.ts
1909
2035
  function stripComments(content) {
1910
2036
  let result = "";
@@ -2557,6 +2683,7 @@ function scanEntropy(files) {
2557
2683
  if (SKIP_FILENAMES.test(basename)) continue;
2558
2684
  if (filePath.includes("node_modules")) continue;
2559
2685
  if (filePath.includes(".min.")) continue;
2686
+ if (/pro-rules-bundle|\.bundle\.|\.chunk\./i.test(filePath)) continue;
2560
2687
  if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
2561
2688
  const lines = content.split("\n");
2562
2689
  for (let i = 0; i < lines.length; i++) {
@@ -3500,6 +3627,29 @@ async function scanCommand(directory, options) {
3500
3627
  chalk2.gray("Tip: Set ANTHROPIC_API_KEY for AI-powered contextual analysis")
3501
3628
  );
3502
3629
  }
3630
+ let aiReviewed = false;
3631
+ let aiRemovedCount = 0;
3632
+ if (process.env.ANTHROPIC_API_KEY && allFindings.length > 0) {
3633
+ spinner.text = "AI reviewing findings for false positives...";
3634
+ spinner.color = "cyan";
3635
+ try {
3636
+ const fileContentsMap = /* @__PURE__ */ new Map();
3637
+ for (const f of allFindings) {
3638
+ if (!fileContentsMap.has(f.file)) {
3639
+ const content = readFileContents(dir, f.file);
3640
+ if (content) fileContentsMap.set(f.file, content);
3641
+ }
3642
+ }
3643
+ const result2 = await filterFalsePositives(allFindings, fileContentsMap);
3644
+ aiReviewed = result2.aiReviewed;
3645
+ aiRemovedCount = result2.removedCount;
3646
+ if (result2.removedCount > 0) {
3647
+ allFindings.length = 0;
3648
+ allFindings.push(...result2.findings);
3649
+ }
3650
+ } catch {
3651
+ }
3652
+ }
3503
3653
  spinner.stop();
3504
3654
  const seen = /* @__PURE__ */ new Set();
3505
3655
  const dedupedFindings = allFindings.filter((f) => {
@@ -3526,12 +3676,17 @@ async function scanCommand(directory, options) {
3526
3676
  renderTerminalReport(result, fileContentsForAnalysis);
3527
3677
  break;
3528
3678
  }
3679
+ if (aiReviewed && !isSilent) {
3680
+ const msg = aiRemovedCount > 0 ? ` AI review: ${aiRemovedCount} false positive${aiRemovedCount !== 1 ? "s" : ""} removed, ${dedupedFindings.length} verified finding${dedupedFindings.length !== 1 ? "s" : ""} remain` : ` AI review: all ${dedupedFindings.length} finding${dedupedFindings.length !== 1 ? "s" : ""} verified`;
3681
+ console.log("");
3682
+ console.log(chalk2.cyan("\u{1F916} ") + chalk2.gray(msg));
3683
+ }
3529
3684
  if (tier === "free" && !isSilent) {
3530
3685
  console.log("");
3531
3686
  if (userPlan === "anonymous") {
3532
- console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 151 rules \u2192") + chalk2.bold(" xploitscan auth login"));
3687
+ 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
3688
  } else {
3534
- console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 151 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
3689
+ console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 158 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
3535
3690
  }
3536
3691
  console.log("");
3537
3692
  }