xploitscan 1.2.2 → 1.3.1

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 CHANGED
@@ -13,13 +13,15 @@ import {
13
13
  getStoredToken,
14
14
  incrementUsage,
15
15
  isAuthenticated,
16
+ isVendoredFinding,
16
17
  loadCachedProRules,
17
18
  runCustomRules,
18
19
  scanEntropy,
19
20
  storeToken,
20
21
  syncUser,
21
- uploadScanResults
22
- } from "./chunk-4OFR3SIS.js";
22
+ uploadScanResults,
23
+ vendoredReason
24
+ } from "./chunk-2VDAYFPC.js";
23
25
 
24
26
  // src/index.ts
25
27
  import { Command } from "commander";
@@ -313,6 +315,9 @@ function mergeRcIntoConfig(base, rc) {
313
315
  .../* @__PURE__ */ new Set([...merged.extensions ?? [], ...rc.scan.extensions])
314
316
  ];
315
317
  }
318
+ if (rc.scan.gradeVendored !== void 0) {
319
+ merged.gradeVendored = rc.scan.gradeVendored;
320
+ }
316
321
  }
317
322
  if (rc.output) {
318
323
  if (rc.output.format !== void 0) {
@@ -622,6 +627,14 @@ ${existingFindings.map((f) => `- ${f.file}:${f.line} \u2014 ${f.title}`).join("\
622
627
  const response = await client.messages.create({
623
628
  model: "claude-sonnet-4-5-20250514",
624
629
  max_tokens: 4096,
630
+ // Pinned for the same reason as the FP filter, and the stakes are
631
+ // higher here: findings returned by this analyzer are pushed onto
632
+ // allFindings in commands/scan.ts, which is what calculateGrade()
633
+ // scores. Left at the API default of 1.0, a user could scan the same
634
+ // unchanged commit twice and get a different letter grade — with
635
+ // nothing in the output to explain why. Discovery arguably benefits
636
+ // from sampling variety; a grade the user is asked to trust does not.
637
+ temperature: 0,
625
638
  messages: [
626
639
  {
627
640
  role: "user",
@@ -1759,8 +1772,12 @@ var GRADE_COLORS = {
1759
1772
  "D": chalk.red.bold,
1760
1773
  "F": chalk.bgRed.white.bold
1761
1774
  };
1762
- function renderTerminalReport(result, files) {
1775
+ function renderTerminalReport(result, files, options = {}) {
1763
1776
  const { findings, filesScanned, duration } = result;
1777
+ const gradeVendored = options.gradeVendored ?? false;
1778
+ const isHeldOut = (f) => !gradeVendored && isVendoredFinding(f);
1779
+ const gradedFindings = findings.filter((f) => !isHeldOut(f));
1780
+ const heldOutFindings = findings.filter(isHeldOut);
1764
1781
  console.log("");
1765
1782
  console.log(chalk.bold.cyan(" xploitscan") + chalk.gray(" \u2014 security scan results"));
1766
1783
  console.log(chalk.gray(" " + "\u2500".repeat(50)));
@@ -1787,11 +1804,18 @@ function renderTerminalReport(result, files) {
1787
1804
  }
1788
1805
  }
1789
1806
  const GRADE_PERCENTILES = { "A+": 98, "A": 90, "B": 70, "C": 45, "D": 20, "F": 5 };
1790
- const { grade, score, summary } = calculateGrade(findings, filesScanned);
1807
+ const { grade, score, summary } = calculateGrade(findings, filesScanned, { gradeVendored });
1791
1808
  const gradeColor = GRADE_COLORS[grade];
1792
1809
  const percentile = GRADE_PERCENTILES[grade] ?? 50;
1793
1810
  console.log(chalk.gray(" Security Grade: ") + gradeColor(` ${grade} `) + chalk.gray(` (${score}/100) \u2014 ${summary}`));
1794
1811
  console.log(chalk.gray(` Benchmark: More secure than ${percentile}% of projects scanned`));
1812
+ if (heldOutFindings.length > 0) {
1813
+ console.log(
1814
+ chalk.gray(
1815
+ ` Not graded: ${heldOutFindings.length} finding${heldOutFindings.length === 1 ? "" : "s"} in vendored, generated, or documentation files (listed below, tagged ${chalk.dim("[not graded]")}). Use --grade-vendored to include them.`
1816
+ )
1817
+ );
1818
+ }
1795
1819
  console.log("");
1796
1820
  if (findings.length === 0) {
1797
1821
  console.log(chalk.green.bold(" No vulnerabilities found!"));
@@ -1799,18 +1823,23 @@ function renderTerminalReport(result, files) {
1799
1823
  console.log("");
1800
1824
  return;
1801
1825
  }
1802
- const sorted = [...findings].sort(
1803
- (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
1804
- );
1826
+ const sorted = [...findings].sort((a, b) => {
1827
+ const heldDiff = Number(isHeldOut(a)) - Number(isHeldOut(b));
1828
+ if (heldDiff !== 0) return heldDiff;
1829
+ return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
1830
+ });
1805
1831
  const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
1806
- for (const f of findings) counts[f.severity]++;
1832
+ for (const f of gradedFindings) counts[f.severity]++;
1807
1833
  const summaryParts = [];
1808
1834
  if (counts.critical > 0) summaryParts.push(chalk.bgRed.white.bold(` ${counts.critical} CRITICAL `));
1809
1835
  if (counts.high > 0) summaryParts.push(chalk.red.bold(`${counts.high} high`));
1810
1836
  if (counts.medium > 0) summaryParts.push(chalk.yellow.bold(`${counts.medium} medium`));
1811
1837
  if (counts.low > 0) summaryParts.push(chalk.blue(`${counts.low} low`));
1812
1838
  if (counts.info > 0) summaryParts.push(chalk.gray(`${counts.info} info`));
1813
- console.log(` Found ${chalk.bold(findings.length.toString())} issues: ${summaryParts.join(chalk.gray(" | "))}`);
1839
+ const heldOutSuffix = heldOutFindings.length > 0 ? chalk.gray(` + ${heldOutFindings.length} not graded`) : "";
1840
+ console.log(
1841
+ gradedFindings.length === 0 ? ` ${chalk.green.bold("No issues in your code")}${heldOutSuffix}` : ` Found ${chalk.bold(gradedFindings.length.toString())} issue${gradedFindings.length === 1 ? "" : "s"} in your code: ${summaryParts.join(chalk.gray(" | "))}${heldOutSuffix}`
1842
+ );
1814
1843
  console.log(chalk.gray(` Scanned ${filesScanned} files in ${(duration / 1e3).toFixed(1)}s`));
1815
1844
  console.log("");
1816
1845
  for (const finding of sorted) {
@@ -1823,8 +1852,12 @@ function renderTerminalReport(result, files) {
1823
1852
  finding.owasp ? chalk.yellow(`[${finding.owasp}]`) : "",
1824
1853
  finding.cwe ? chalk.blue(`[${finding.cwe}]`) : ""
1825
1854
  ].filter(Boolean).join(" ");
1826
- console.log(` ${severityLabel}${sourceLabel}${chalk.bold(finding.title)} ${complianceTags}${confidenceLabel}`);
1827
- console.log(chalk.gray(` ${finding.file}:${finding.line}`));
1855
+ const heldOutLabel = isHeldOut(finding) ? chalk.dim(" [not graded] ") : "";
1856
+ console.log(` ${severityLabel}${heldOutLabel}${sourceLabel}${chalk.bold(finding.title)} ${complianceTags}${confidenceLabel}`);
1857
+ const reason = isHeldOut(finding) ? vendoredReason(finding.file) : null;
1858
+ console.log(
1859
+ chalk.gray(` ${finding.file}:${finding.line}`) + (reason ? chalk.dim(` \u2014 ${reason}, not counted toward the grade`) : "")
1860
+ );
1828
1861
  console.log("");
1829
1862
  console.log(chalk.white(` ${finding.description}`));
1830
1863
  console.log("");
@@ -1846,7 +1879,7 @@ function renderTerminalReport(result, files) {
1846
1879
  console.log(chalk.gray(" " + "\u2500".repeat(50)));
1847
1880
  console.log("");
1848
1881
  }
1849
- const owaspFindings = findings.filter((f) => f.owasp);
1882
+ const owaspFindings = gradedFindings.filter((f) => f.owasp);
1850
1883
  if (owaspFindings.length > 0) {
1851
1884
  const owaspCats = {
1852
1885
  "A01:2021": { name: "Broken Access Control", count: 0 },
@@ -1941,6 +1974,14 @@ Suggested fix: ${f.fix}` : `${f.title}: ${f.description}`;
1941
1974
  ruleIndex: ruleIndex.get(f.rule) ?? 0,
1942
1975
  level: SEVERITY_TO_SARIF[f.severity],
1943
1976
  message: { text: messageText },
1977
+ // Vendored/generated/docs findings are reported, not suppressed:
1978
+ // they keep their real `level` so nothing is hidden from a SARIF
1979
+ // consumer, and carry a property so a consumer that wants to
1980
+ // filter them out can. We deliberately don't emit SARIF
1981
+ // `suppressions[]` — that would make GitHub code scanning drop
1982
+ // them from the UI entirely, which is the silent-hiding we're
1983
+ // trying to avoid.
1984
+ ...f.vendored ? { properties: { vendored: true, gradeExcluded: true } } : {},
1944
1985
  locations: [
1945
1986
  {
1946
1987
  physicalLocation: {
@@ -2336,12 +2377,19 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2336
2377
  )
2337
2378
  );
2338
2379
  }
2380
+ const gradeVendored = options.gradeVendored ?? config.gradeVendored ?? false;
2381
+ for (const f of dedupedFindings) {
2382
+ if (isVendoredFinding(f)) f.vendored = true;
2383
+ else delete f.vendored;
2384
+ }
2385
+ const ungradedFindings = gradeVendored ? [] : dedupedFindings.filter((f) => isVendoredFinding(f));
2339
2386
  const result = {
2340
2387
  findings: dedupedFindings,
2341
2388
  filesScanned: files.length,
2342
2389
  duration: Date.now() - startTime,
2343
2390
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2344
- directory: dir
2391
+ directory: dir,
2392
+ ungraded: ungradedFindings.length
2345
2393
  };
2346
2394
  const PLATFORM_MARKERS = [
2347
2395
  ".cursorrules",
@@ -2371,7 +2419,7 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2371
2419
  renderDatadogReport(result);
2372
2420
  break;
2373
2421
  default:
2374
- renderTerminalReport(result, [...fileContentsForAnalysis, ...platformMarkerFiles]);
2422
+ renderTerminalReport(result, [...fileContentsForAnalysis, ...platformMarkerFiles], { gradeVendored });
2375
2423
  break;
2376
2424
  }
2377
2425
  if (aiReviewed && !isSilent) {
@@ -2414,7 +2462,8 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2414
2462
  })
2415
2463
  ]);
2416
2464
  }
2417
- const hasCritical = dedupedFindings.some(
2465
+ const gatedFindings = gradeVendored ? dedupedFindings : dedupedFindings.filter((f) => !isVendoredFinding(f));
2466
+ const hasCritical = gatedFindings.some(
2418
2467
  (f) => f.severity === "critical" || f.severity === "high"
2419
2468
  );
2420
2469
  if (hasCritical) {
@@ -2785,19 +2834,23 @@ async function cursorInstallCommand(opts = {}) {
2785
2834
  var program = new Command();
2786
2835
  program.name("xploitscan").description(
2787
2836
  "AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
2788
- ).version("1.2.2");
2837
+ ).version("1.3.1");
2789
2838
  program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option(
2790
2839
  "-f, --format <format>",
2791
2840
  "Output format: terminal, json, sarif, splunk-hec, elastic-ecs, datadog-logs",
2792
2841
  "terminal"
2793
- ).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) => {
2842
+ ).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).option(
2843
+ "--grade-vendored",
2844
+ "Count findings in vendored/generated/docs files (components/ui, __generated__, *.md) toward the grade and exit code. Off by default \u2014 those findings are reported either way."
2845
+ ).action(async (directory, opts) => {
2794
2846
  await scanCommand(directory, {
2795
2847
  directory,
2796
2848
  aiAnalysis: opts.ai,
2797
2849
  format: opts.format,
2798
2850
  verbose: opts.verbose,
2799
2851
  diff: opts.diff,
2800
- watch: opts.watch
2852
+ watch: opts.watch,
2853
+ gradeVendored: opts.gradeVendored
2801
2854
  });
2802
2855
  });
2803
2856
  var auth = program.command("auth").description("Manage authentication");
@@ -2814,7 +2867,7 @@ cursor.command("install").description("Drop XploitScan security rules into .curs
2814
2867
  await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });
2815
2868
  });
2816
2869
  program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
2817
- const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-MFCSQQ2Y.js");
2870
+ const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-6FD27EJY.js");
2818
2871
  const chalk6 = (await import("chalk")).default;
2819
2872
  const token = getStoredToken2();
2820
2873
  if (!token) {