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/README.md +44 -0
- package/dist/{api-MFCSQQ2Y.js → api-AX6R4QAD.js} +2 -2
- package/dist/{chunk-4OFR3SIS.js → chunk-LWX7UPO5.js} +188 -26
- package/dist/chunk-LWX7UPO5.js.map +1 -0
- package/dist/index.js +65 -20
- 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-AX6R4QAD.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-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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1827
|
-
console.log(
|
|
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 =
|
|
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("
|
|
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
|
|
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.
|
|
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).
|
|
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-
|
|
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) {
|