xploitscan 1.2.1 → 1.3.0

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-LWX7UPO5.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) {
@@ -1759,8 +1764,12 @@ var GRADE_COLORS = {
1759
1764
  "D": chalk.red.bold,
1760
1765
  "F": chalk.bgRed.white.bold
1761
1766
  };
1762
- function renderTerminalReport(result, files) {
1767
+ function renderTerminalReport(result, files, options = {}) {
1763
1768
  const { findings, filesScanned, duration } = result;
1769
+ const gradeVendored = options.gradeVendored ?? false;
1770
+ const isHeldOut = (f) => !gradeVendored && isVendoredFinding(f);
1771
+ const gradedFindings = findings.filter((f) => !isHeldOut(f));
1772
+ const heldOutFindings = findings.filter(isHeldOut);
1764
1773
  console.log("");
1765
1774
  console.log(chalk.bold.cyan(" xploitscan") + chalk.gray(" \u2014 security scan results"));
1766
1775
  console.log(chalk.gray(" " + "\u2500".repeat(50)));
@@ -1787,11 +1796,18 @@ function renderTerminalReport(result, files) {
1787
1796
  }
1788
1797
  }
1789
1798
  const GRADE_PERCENTILES = { "A+": 98, "A": 90, "B": 70, "C": 45, "D": 20, "F": 5 };
1790
- const { grade, score, summary } = calculateGrade(findings, filesScanned);
1799
+ const { grade, score, summary } = calculateGrade(findings, filesScanned, { gradeVendored });
1791
1800
  const gradeColor = GRADE_COLORS[grade];
1792
1801
  const percentile = GRADE_PERCENTILES[grade] ?? 50;
1793
1802
  console.log(chalk.gray(" Security Grade: ") + gradeColor(` ${grade} `) + chalk.gray(` (${score}/100) \u2014 ${summary}`));
1794
1803
  console.log(chalk.gray(` Benchmark: More secure than ${percentile}% of projects scanned`));
1804
+ if (heldOutFindings.length > 0) {
1805
+ console.log(
1806
+ chalk.gray(
1807
+ ` 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.`
1808
+ )
1809
+ );
1810
+ }
1795
1811
  console.log("");
1796
1812
  if (findings.length === 0) {
1797
1813
  console.log(chalk.green.bold(" No vulnerabilities found!"));
@@ -1799,18 +1815,23 @@ function renderTerminalReport(result, files) {
1799
1815
  console.log("");
1800
1816
  return;
1801
1817
  }
1802
- const sorted = [...findings].sort(
1803
- (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
1804
- );
1818
+ const sorted = [...findings].sort((a, b) => {
1819
+ const heldDiff = Number(isHeldOut(a)) - Number(isHeldOut(b));
1820
+ if (heldDiff !== 0) return heldDiff;
1821
+ return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
1822
+ });
1805
1823
  const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
1806
- for (const f of findings) counts[f.severity]++;
1824
+ for (const f of gradedFindings) counts[f.severity]++;
1807
1825
  const summaryParts = [];
1808
1826
  if (counts.critical > 0) summaryParts.push(chalk.bgRed.white.bold(` ${counts.critical} CRITICAL `));
1809
1827
  if (counts.high > 0) summaryParts.push(chalk.red.bold(`${counts.high} high`));
1810
1828
  if (counts.medium > 0) summaryParts.push(chalk.yellow.bold(`${counts.medium} medium`));
1811
1829
  if (counts.low > 0) summaryParts.push(chalk.blue(`${counts.low} low`));
1812
1830
  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(" | "))}`);
1831
+ const heldOutSuffix = heldOutFindings.length > 0 ? chalk.gray(` + ${heldOutFindings.length} not graded`) : "";
1832
+ console.log(
1833
+ 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}`
1834
+ );
1814
1835
  console.log(chalk.gray(` Scanned ${filesScanned} files in ${(duration / 1e3).toFixed(1)}s`));
1815
1836
  console.log("");
1816
1837
  for (const finding of sorted) {
@@ -1823,8 +1844,12 @@ function renderTerminalReport(result, files) {
1823
1844
  finding.owasp ? chalk.yellow(`[${finding.owasp}]`) : "",
1824
1845
  finding.cwe ? chalk.blue(`[${finding.cwe}]`) : ""
1825
1846
  ].filter(Boolean).join(" ");
1826
- console.log(` ${severityLabel}${sourceLabel}${chalk.bold(finding.title)} ${complianceTags}${confidenceLabel}`);
1827
- console.log(chalk.gray(` ${finding.file}:${finding.line}`));
1847
+ const heldOutLabel = isHeldOut(finding) ? chalk.dim(" [not graded] ") : "";
1848
+ console.log(` ${severityLabel}${heldOutLabel}${sourceLabel}${chalk.bold(finding.title)} ${complianceTags}${confidenceLabel}`);
1849
+ const reason = isHeldOut(finding) ? vendoredReason(finding.file) : null;
1850
+ console.log(
1851
+ chalk.gray(` ${finding.file}:${finding.line}`) + (reason ? chalk.dim(` \u2014 ${reason}, not counted toward the grade`) : "")
1852
+ );
1828
1853
  console.log("");
1829
1854
  console.log(chalk.white(` ${finding.description}`));
1830
1855
  console.log("");
@@ -1846,7 +1871,7 @@ function renderTerminalReport(result, files) {
1846
1871
  console.log(chalk.gray(" " + "\u2500".repeat(50)));
1847
1872
  console.log("");
1848
1873
  }
1849
- const owaspFindings = findings.filter((f) => f.owasp);
1874
+ const owaspFindings = gradedFindings.filter((f) => f.owasp);
1850
1875
  if (owaspFindings.length > 0) {
1851
1876
  const owaspCats = {
1852
1877
  "A01:2021": { name: "Broken Access Control", count: 0 },
@@ -1941,6 +1966,14 @@ Suggested fix: ${f.fix}` : `${f.title}: ${f.description}`;
1941
1966
  ruleIndex: ruleIndex.get(f.rule) ?? 0,
1942
1967
  level: SEVERITY_TO_SARIF[f.severity],
1943
1968
  message: { text: messageText },
1969
+ // Vendored/generated/docs findings are reported, not suppressed:
1970
+ // they keep their real `level` so nothing is hidden from a SARIF
1971
+ // consumer, and carry a property so a consumer that wants to
1972
+ // filter them out can. We deliberately don't emit SARIF
1973
+ // `suppressions[]` — that would make GitHub code scanning drop
1974
+ // them from the UI entirely, which is the silent-hiding we're
1975
+ // trying to avoid.
1976
+ ...f.vendored ? { properties: { vendored: true, gradeExcluded: true } } : {},
1944
1977
  locations: [
1945
1978
  {
1946
1979
  physicalLocation: {
@@ -2336,12 +2369,19 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2336
2369
  )
2337
2370
  );
2338
2371
  }
2372
+ const gradeVendored = options.gradeVendored ?? config.gradeVendored ?? false;
2373
+ for (const f of dedupedFindings) {
2374
+ if (isVendoredFinding(f)) f.vendored = true;
2375
+ else delete f.vendored;
2376
+ }
2377
+ const ungradedFindings = gradeVendored ? [] : dedupedFindings.filter((f) => isVendoredFinding(f));
2339
2378
  const result = {
2340
2379
  findings: dedupedFindings,
2341
2380
  filesScanned: files.length,
2342
2381
  duration: Date.now() - startTime,
2343
2382
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2344
- directory: dir
2383
+ directory: dir,
2384
+ ungraded: ungradedFindings.length
2345
2385
  };
2346
2386
  const PLATFORM_MARKERS = [
2347
2387
  ".cursorrules",
@@ -2371,7 +2411,7 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2371
2411
  renderDatadogReport(result);
2372
2412
  break;
2373
2413
  default:
2374
- renderTerminalReport(result, [...fileContentsForAnalysis, ...platformMarkerFiles]);
2414
+ renderTerminalReport(result, [...fileContentsForAnalysis, ...platformMarkerFiles], { gradeVendored });
2375
2415
  break;
2376
2416
  }
2377
2417
  if (aiReviewed && !isSilent) {
@@ -2397,7 +2437,7 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2397
2437
  if (tier === "free" && !isSilent) {
2398
2438
  console.log("");
2399
2439
  if (userPlan === "anonymous") {
2400
- console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 210 rules \u2192") + chalk2.bold(" xploitscan auth login"));
2440
+ console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Pro unlocks all 210 \u2192") + chalk2.bold(" xploitscan upgrade"));
2401
2441
  } else {
2402
2442
  console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 210 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
2403
2443
  }
@@ -2414,7 +2454,8 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
2414
2454
  })
2415
2455
  ]);
2416
2456
  }
2417
- const hasCritical = dedupedFindings.some(
2457
+ const gatedFindings = gradeVendored ? dedupedFindings : dedupedFindings.filter((f) => !isVendoredFinding(f));
2458
+ const hasCritical = gatedFindings.some(
2418
2459
  (f) => f.severity === "critical" || f.severity === "high"
2419
2460
  );
2420
2461
  if (hasCritical) {
@@ -2785,19 +2826,23 @@ async function cursorInstallCommand(opts = {}) {
2785
2826
  var program = new Command();
2786
2827
  program.name("xploitscan").description(
2787
2828
  "AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
2788
- ).version("1.2.1");
2829
+ ).version("1.3.0");
2789
2830
  program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option(
2790
2831
  "-f, --format <format>",
2791
2832
  "Output format: terminal, json, sarif, splunk-hec, elastic-ecs, datadog-logs",
2792
2833
  "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) => {
2834
+ ).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(
2835
+ "--grade-vendored",
2836
+ "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."
2837
+ ).action(async (directory, opts) => {
2794
2838
  await scanCommand(directory, {
2795
2839
  directory,
2796
2840
  aiAnalysis: opts.ai,
2797
2841
  format: opts.format,
2798
2842
  verbose: opts.verbose,
2799
2843
  diff: opts.diff,
2800
- watch: opts.watch
2844
+ watch: opts.watch,
2845
+ gradeVendored: opts.gradeVendored
2801
2846
  });
2802
2847
  });
2803
2848
  var auth = program.command("auth").description("Manage authentication");
@@ -2814,7 +2859,7 @@ cursor.command("install").description("Drop XploitScan security rules into .curs
2814
2859
  await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });
2815
2860
  });
2816
2861
  program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
2817
- const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-MFCSQQ2Y.js");
2862
+ const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-AX6R4QAD.js");
2818
2863
  const chalk6 = (await import("chalk")).default;
2819
2864
  const token = getStoredToken2();
2820
2865
  if (!token) {