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/README.md +44 -0
- package/dist/{api-MFCSQQ2Y.js → api-6FD27EJY.js} +2 -2
- package/dist/{chunk-4OFR3SIS.js → chunk-2VDAYFPC.js} +199 -26
- package/dist/chunk-2VDAYFPC.js.map +1 -0
- package/dist/index.js +72 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-4OFR3SIS.js.map +0 -1
- /package/dist/{api-MFCSQQ2Y.js.map → api-6FD27EJY.js.map} +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1827
|
-
console.log(
|
|
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 =
|
|
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
|
|
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.
|
|
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).
|
|
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-
|
|
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) {
|