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 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.4");
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 chalk5 = (await import("chalk")).default;
3849
+ const chalk6 = (await import("chalk")).default;
3618
3850
  const token = getStoredToken2();
3619
3851
  if (!token) {
3620
- console.log(chalk5.yellow("Please log in first: xploitscan auth login"));
3852
+ console.log(chalk6.yellow("Please log in first: xploitscan auth login"));
3621
3853
  return;
3622
3854
  }
3623
- console.log(chalk5.cyan("Creating checkout session..."));
3855
+ console.log(chalk6.cyan("Creating checkout session..."));
3624
3856
  const url = await getCheckoutUrl();
3625
3857
  if (url) {
3626
- console.log(chalk5.green(`
3858
+ console.log(chalk6.green(`
3627
3859
  Open this URL to upgrade:`));
3628
- console.log(chalk5.bold.underline(url));
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(chalk5.red("Failed to create checkout session. Please try again."));
3866
+ console.log(chalk6.red("Failed to create checkout session. Please try again."));
3635
3867
  }
3636
3868
  });
3637
3869
  program.parse();