xploitscan 1.0.4 → 1.0.7
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 +239 -7
- package/dist/index.js.map +1 -1
- package/dist/templates/cursor-security.mdc +112 -0
- package/dist/templates/cursorrules-legacy.txt +51 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1925,6 +1925,85 @@ function buildASTContext(content, filePath) {
|
|
|
1925
1925
|
};
|
|
1926
1926
|
}
|
|
1927
1927
|
|
|
1928
|
+
// src/scanners/osv.ts
|
|
1929
|
+
var OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
1930
|
+
var OSV_VULN_URL = "https://api.osv.dev/v1/vulns";
|
|
1931
|
+
var NETWORK_TIMEOUT_MS = 5e3;
|
|
1932
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
1933
|
+
const controller = new AbortController();
|
|
1934
|
+
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1935
|
+
try {
|
|
1936
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1937
|
+
} finally {
|
|
1938
|
+
clearTimeout(id);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async function queryOsvBatch(queries) {
|
|
1942
|
+
if (queries.length === 0) return [];
|
|
1943
|
+
try {
|
|
1944
|
+
const batchRes = await fetchWithTimeout(
|
|
1945
|
+
OSV_BATCH_URL,
|
|
1946
|
+
{
|
|
1947
|
+
method: "POST",
|
|
1948
|
+
headers: { "Content-Type": "application/json" },
|
|
1949
|
+
body: JSON.stringify({ queries })
|
|
1950
|
+
},
|
|
1951
|
+
NETWORK_TIMEOUT_MS
|
|
1952
|
+
);
|
|
1953
|
+
if (!batchRes.ok) return [];
|
|
1954
|
+
const batch = await batchRes.json();
|
|
1955
|
+
if (!Array.isArray(batch.results)) return [];
|
|
1956
|
+
const uniqueVulnIds = /* @__PURE__ */ new Set();
|
|
1957
|
+
for (const result of batch.results) {
|
|
1958
|
+
for (const v of result.vulns ?? []) uniqueVulnIds.add(v.id);
|
|
1959
|
+
}
|
|
1960
|
+
const vulnDetails = /* @__PURE__ */ new Map();
|
|
1961
|
+
for (const vulnId of uniqueVulnIds) {
|
|
1962
|
+
try {
|
|
1963
|
+
const detailRes = await fetchWithTimeout(
|
|
1964
|
+
`${OSV_VULN_URL}/${encodeURIComponent(vulnId)}`,
|
|
1965
|
+
{ method: "GET" },
|
|
1966
|
+
NETWORK_TIMEOUT_MS
|
|
1967
|
+
);
|
|
1968
|
+
if (!detailRes.ok) continue;
|
|
1969
|
+
const detail = await detailRes.json();
|
|
1970
|
+
vulnDetails.set(vulnId, detail);
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
const matches = [];
|
|
1975
|
+
batch.results.forEach((result, i) => {
|
|
1976
|
+
const ids = (result.vulns ?? []).map((v) => v.id);
|
|
1977
|
+
if (ids.length === 0) return;
|
|
1978
|
+
const q = queries[i];
|
|
1979
|
+
matches.push({
|
|
1980
|
+
ecosystem: q.package.ecosystem,
|
|
1981
|
+
name: q.package.name,
|
|
1982
|
+
version: q.version,
|
|
1983
|
+
vulns: ids.map((id) => vulnDetails.get(id) ?? { id })
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
return matches;
|
|
1987
|
+
} catch {
|
|
1988
|
+
return [];
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
function osvSeverity(vuln) {
|
|
1992
|
+
const vectors = vuln.severity ?? [];
|
|
1993
|
+
for (const v of vectors) {
|
|
1994
|
+
const match = v.score.match(/\b(\d+(?:\.\d+)?)\b/);
|
|
1995
|
+
if (!match) continue;
|
|
1996
|
+
const score = parseFloat(match[1]);
|
|
1997
|
+
if (Number.isFinite(score)) {
|
|
1998
|
+
if (score >= 9) return "critical";
|
|
1999
|
+
if (score >= 7) return "high";
|
|
2000
|
+
if (score >= 4) return "medium";
|
|
2001
|
+
return "low";
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return "medium";
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1928
2007
|
// src/scanners/dependency-scanner.ts
|
|
1929
2008
|
var KNOWN_VULN_NPM = {
|
|
1930
2009
|
"lodash": [{
|
|
@@ -2225,6 +2304,94 @@ function scanDependencies(files) {
|
|
|
2225
2304
|
}
|
|
2226
2305
|
return findings;
|
|
2227
2306
|
}
|
|
2307
|
+
async function scanDependenciesOsv(files, alreadyFoundByRule) {
|
|
2308
|
+
const lookups = [];
|
|
2309
|
+
for (const { path: filePath, content } of files) {
|
|
2310
|
+
if (filePath.endsWith("package.json") && !filePath.includes("node_modules")) {
|
|
2311
|
+
try {
|
|
2312
|
+
const pkg = JSON.parse(content);
|
|
2313
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2314
|
+
for (const [name, rawVersion] of Object.entries(allDeps)) {
|
|
2315
|
+
const version = String(rawVersion).replace(/^[\^~>=<]*/g, "").trim();
|
|
2316
|
+
if (!version || version === "*" || version === "latest") continue;
|
|
2317
|
+
const line = content.split("\n").findIndex((l) => l.includes(`"${name}"`)) + 1;
|
|
2318
|
+
lookups.push({ ecosystem: "npm", name, version, file: filePath, line: line || 1, content });
|
|
2319
|
+
}
|
|
2320
|
+
} catch {
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (filePath.match(/requirements.*\.txt$/i)) {
|
|
2324
|
+
const lines = content.split("\n");
|
|
2325
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2326
|
+
const m = lines[i].trim().match(/^([a-zA-Z0-9_-]+)==([\d.]+)/);
|
|
2327
|
+
if (!m) continue;
|
|
2328
|
+
lookups.push({
|
|
2329
|
+
ecosystem: "PyPI",
|
|
2330
|
+
name: m[1].toLowerCase(),
|
|
2331
|
+
version: m[2],
|
|
2332
|
+
file: filePath,
|
|
2333
|
+
line: i + 1,
|
|
2334
|
+
content
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (filePath.endsWith("Gemfile.lock")) {
|
|
2339
|
+
const lines = content.split("\n");
|
|
2340
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2341
|
+
const m = lines[i].match(/^\s{4}([a-z0-9_-]+)\s+\(([\d.]+)\)/);
|
|
2342
|
+
if (!m) continue;
|
|
2343
|
+
lookups.push({
|
|
2344
|
+
ecosystem: "RubyGems",
|
|
2345
|
+
name: m[1],
|
|
2346
|
+
version: m[2],
|
|
2347
|
+
file: filePath,
|
|
2348
|
+
line: i + 1,
|
|
2349
|
+
content
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
if (lookups.length === 0) return [];
|
|
2355
|
+
const CHUNK = 500;
|
|
2356
|
+
const findings = [];
|
|
2357
|
+
for (let start = 0; start < lookups.length; start += CHUNK) {
|
|
2358
|
+
const chunk = lookups.slice(start, start + CHUNK);
|
|
2359
|
+
const matches = await queryOsvBatch(
|
|
2360
|
+
chunk.map((l) => ({ package: { name: l.name, ecosystem: l.ecosystem }, version: l.version }))
|
|
2361
|
+
);
|
|
2362
|
+
const matchByKey = /* @__PURE__ */ new Map();
|
|
2363
|
+
for (const m of matches) matchByKey.set(`${m.ecosystem}:${m.name}:${m.version}`, m);
|
|
2364
|
+
for (const lookup of chunk) {
|
|
2365
|
+
const key = `${lookup.ecosystem}:${lookup.name}:${lookup.version}`;
|
|
2366
|
+
const match = matchByKey.get(key);
|
|
2367
|
+
if (!match) continue;
|
|
2368
|
+
for (const vuln of match.vulns) {
|
|
2369
|
+
const dedupeKey = `${lookup.name}:${vuln.id}`;
|
|
2370
|
+
if (alreadyFoundByRule.has(dedupeKey)) continue;
|
|
2371
|
+
alreadyFoundByRule.add(dedupeKey);
|
|
2372
|
+
const severity = osvSeverity(vuln);
|
|
2373
|
+
const title = vuln.summary || `${lookup.name} vulnerability ${vuln.id}`;
|
|
2374
|
+
const description = vuln.details || vuln.summary || `${lookup.name}@${lookup.version} is affected by ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id} for details.`;
|
|
2375
|
+
findings.push({
|
|
2376
|
+
id: `${vuln.id}-${lookup.file}:${lookup.line}`,
|
|
2377
|
+
rule: vuln.id,
|
|
2378
|
+
severity,
|
|
2379
|
+
title,
|
|
2380
|
+
description: description.slice(0, 500),
|
|
2381
|
+
file: lookup.file,
|
|
2382
|
+
line: lookup.line,
|
|
2383
|
+
snippet: getSnippet2(lookup.content, lookup.line),
|
|
2384
|
+
fix: `Upgrade ${lookup.name} to a version that resolves ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id}`,
|
|
2385
|
+
category: "Dependencies",
|
|
2386
|
+
source: "custom",
|
|
2387
|
+
owasp: "A06:2021",
|
|
2388
|
+
cwe: "CWE-1395"
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return findings;
|
|
2394
|
+
}
|
|
2228
2395
|
|
|
2229
2396
|
// src/scanners/entropy-scanner.ts
|
|
2230
2397
|
function shannonEntropy(str) {
|
|
@@ -3136,6 +3303,24 @@ async function scanCommand(directory, options) {
|
|
|
3136
3303
|
f.confidence = "high";
|
|
3137
3304
|
}
|
|
3138
3305
|
allFindings.push(...depFindings);
|
|
3306
|
+
const dedupeKeys = /* @__PURE__ */ new Set();
|
|
3307
|
+
for (const f of depFindings) {
|
|
3308
|
+
const match = f.title.match(/^(\S+)/);
|
|
3309
|
+
if (match) dedupeKeys.add(`${match[1].toLowerCase()}:${f.rule}`);
|
|
3310
|
+
}
|
|
3311
|
+
spinner.text = "Checking dependencies against OSV.dev...";
|
|
3312
|
+
try {
|
|
3313
|
+
const osvFindings = await scanDependenciesOsv(fileContentsForAnalysis, dedupeKeys);
|
|
3314
|
+
for (const f of osvFindings) {
|
|
3315
|
+
f.confidence = "high";
|
|
3316
|
+
}
|
|
3317
|
+
allFindings.push(...osvFindings);
|
|
3318
|
+
if (verbose && osvFindings.length > 0) {
|
|
3319
|
+
spinner.info(`OSV.dev found ${osvFindings.length} additional vulnerable dependencies`);
|
|
3320
|
+
}
|
|
3321
|
+
} catch (err) {
|
|
3322
|
+
if (verbose) spinner.info(`OSV.dev lookup skipped: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
3323
|
+
}
|
|
3139
3324
|
if (verbose && depFindings.length > 0) {
|
|
3140
3325
|
spinner.info(`Dependency scanner found ${depFindings.length} issues`);
|
|
3141
3326
|
}
|
|
@@ -3588,11 +3773,54 @@ async function uninstallHookCommand() {
|
|
|
3588
3773
|
}
|
|
3589
3774
|
}
|
|
3590
3775
|
|
|
3776
|
+
// src/commands/cursor.ts
|
|
3777
|
+
import chalk5 from "chalk";
|
|
3778
|
+
import { mkdirSync, existsSync as existsSync5, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
3779
|
+
import { join as join7, dirname } from "path";
|
|
3780
|
+
import { fileURLToPath } from "url";
|
|
3781
|
+
async function cursorInstallCommand(opts = {}) {
|
|
3782
|
+
const cwd = process.cwd();
|
|
3783
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
3784
|
+
const candidates = [
|
|
3785
|
+
join7(here, "templates"),
|
|
3786
|
+
join7(here, "..", "templates"),
|
|
3787
|
+
join7(here, "..", "src", "templates")
|
|
3788
|
+
];
|
|
3789
|
+
const templatesDir = candidates.find((p) => existsSync5(join7(p, "cursor-security.mdc")));
|
|
3790
|
+
if (!templatesDir) {
|
|
3791
|
+
console.error(chalk5.red("Could not locate XploitScan rule templates. Try reinstalling: npm i -g xploitscan@latest"));
|
|
3792
|
+
process.exit(1);
|
|
3793
|
+
}
|
|
3794
|
+
const mdcSrc = readFileSync4(join7(templatesDir, "cursor-security.mdc"), "utf-8");
|
|
3795
|
+
const legacySrc = readFileSync4(join7(templatesDir, "cursorrules-legacy.txt"), "utf-8");
|
|
3796
|
+
if (!opts.legacyOnly) {
|
|
3797
|
+
const mdcDir = join7(cwd, ".cursor", "rules");
|
|
3798
|
+
const mdcPath = join7(mdcDir, "xploitscan-security.mdc");
|
|
3799
|
+
if (existsSync5(mdcPath) && !opts.force) {
|
|
3800
|
+
console.log(chalk5.yellow(`Skipping ${mdcPath} (already exists, use --force to overwrite)`));
|
|
3801
|
+
} else {
|
|
3802
|
+
mkdirSync(mdcDir, { recursive: true });
|
|
3803
|
+
writeFileSync2(mdcPath, mdcSrc);
|
|
3804
|
+
console.log(chalk5.green(`Installed ${mdcPath}`));
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const legacyPath = join7(cwd, ".cursorrules");
|
|
3808
|
+
if (existsSync5(legacyPath) && !opts.force) {
|
|
3809
|
+
console.log(chalk5.yellow(`Skipping ${legacyPath} (already exists, use --force to overwrite)`));
|
|
3810
|
+
} else {
|
|
3811
|
+
writeFileSync2(legacyPath, legacySrc);
|
|
3812
|
+
console.log(chalk5.green(`Installed ${legacyPath}`));
|
|
3813
|
+
}
|
|
3814
|
+
console.log("");
|
|
3815
|
+
console.log(chalk5.cyan("Cursor will pick up these rules automatically the next time you open the project."));
|
|
3816
|
+
console.log(chalk5.gray("Run a scan any time to catch what slipped through: npx xploitscan scan ."));
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3591
3819
|
// src/index.ts
|
|
3592
3820
|
var program = new Command();
|
|
3593
3821
|
program.name("xploitscan").description(
|
|
3594
3822
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
3595
|
-
).version("1.0.
|
|
3823
|
+
).version("1.0.7");
|
|
3596
3824
|
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option("-f, --format <format>", "Output format: terminal, json, sarif", "terminal").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) => {
|
|
3597
3825
|
await scanCommand(directory, {
|
|
3598
3826
|
directory,
|
|
@@ -3612,26 +3840,30 @@ hook.command("install").description("Install a git pre-commit hook that runs Xpl
|
|
|
3612
3840
|
await installHookCommand({ force: opts.force });
|
|
3613
3841
|
});
|
|
3614
3842
|
hook.command("uninstall").description("Remove the XploitScan pre-commit hook").action(uninstallHookCommand);
|
|
3843
|
+
var cursor = program.command("cursor").description("Manage XploitScan integration with Cursor IDE");
|
|
3844
|
+
cursor.command("install").description("Drop XploitScan security rules into .cursor/rules and .cursorrules so Cursor enforces them at write-time").option("-f, --force", "Overwrite existing rule files", false).option("--legacy-only", "Only install the legacy .cursorrules file (skip .cursor/rules/*.mdc)", false).action(async (opts) => {
|
|
3845
|
+
await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });
|
|
3846
|
+
});
|
|
3615
3847
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
3616
3848
|
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-HTHCG6QE.js");
|
|
3617
|
-
const
|
|
3849
|
+
const chalk6 = (await import("chalk")).default;
|
|
3618
3850
|
const token = getStoredToken2();
|
|
3619
3851
|
if (!token) {
|
|
3620
|
-
console.log(
|
|
3852
|
+
console.log(chalk6.yellow("Please log in first: xploitscan auth login"));
|
|
3621
3853
|
return;
|
|
3622
3854
|
}
|
|
3623
|
-
console.log(
|
|
3855
|
+
console.log(chalk6.cyan("Creating checkout session..."));
|
|
3624
3856
|
const url = await getCheckoutUrl();
|
|
3625
3857
|
if (url) {
|
|
3626
|
-
console.log(
|
|
3858
|
+
console.log(chalk6.green(`
|
|
3627
3859
|
Open this URL to upgrade:`));
|
|
3628
|
-
console.log(
|
|
3860
|
+
console.log(chalk6.bold.underline(url));
|
|
3629
3861
|
const { execFile: execFile4 } = await import("child_process");
|
|
3630
3862
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3631
3863
|
execFile4(openCmd, [url], () => {
|
|
3632
3864
|
});
|
|
3633
3865
|
} else {
|
|
3634
|
-
console.log(
|
|
3866
|
+
console.log(chalk6.red("Failed to create checkout session. Please try again."));
|
|
3635
3867
|
}
|
|
3636
3868
|
});
|
|
3637
3869
|
program.parse();
|