xploitscan 1.0.3 → 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) {
@@ -3003,13 +3170,13 @@ function renderSarifReport(result) {
3003
3170
  }
3004
3171
  },
3005
3172
  results: result.findings.map((f) => {
3006
- const entry = {
3173
+ const messageText = f.fix ? `${f.title}: ${f.description}
3174
+ Suggested fix: ${f.fix}` : `${f.title}: ${f.description}`;
3175
+ return {
3007
3176
  ruleId: f.rule,
3008
3177
  ruleIndex: ruleIndex.get(f.rule) ?? 0,
3009
3178
  level: SEVERITY_TO_SARIF[f.severity],
3010
- message: {
3011
- text: `${f.title}: ${f.description}`
3012
- },
3179
+ message: { text: messageText },
3013
3180
  locations: [
3014
3181
  {
3015
3182
  physicalLocation: {
@@ -3022,10 +3189,6 @@ function renderSarifReport(result) {
3022
3189
  }
3023
3190
  ]
3024
3191
  };
3025
- if (f.fix) {
3026
- entry.fixes = [{ description: { text: f.fix } }];
3027
- }
3028
- return entry;
3029
3192
  })
3030
3193
  }
3031
3194
  ]
@@ -3140,6 +3303,24 @@ async function scanCommand(directory, options) {
3140
3303
  f.confidence = "high";
3141
3304
  }
3142
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
+ }
3143
3324
  if (verbose && depFindings.length > 0) {
3144
3325
  spinner.info(`Dependency scanner found ${depFindings.length} issues`);
3145
3326
  }
@@ -3592,11 +3773,54 @@ async function uninstallHookCommand() {
3592
3773
  }
3593
3774
  }
3594
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
+
3595
3819
  // src/index.ts
3596
3820
  var program = new Command();
3597
3821
  program.name("xploitscan").description(
3598
3822
  "AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
3599
- ).version("1.0.3");
3823
+ ).version("1.0.7");
3600
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) => {
3601
3825
  await scanCommand(directory, {
3602
3826
  directory,
@@ -3616,26 +3840,30 @@ hook.command("install").description("Install a git pre-commit hook that runs Xpl
3616
3840
  await installHookCommand({ force: opts.force });
3617
3841
  });
3618
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
+ });
3619
3847
  program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
3620
3848
  const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-HTHCG6QE.js");
3621
- const chalk5 = (await import("chalk")).default;
3849
+ const chalk6 = (await import("chalk")).default;
3622
3850
  const token = getStoredToken2();
3623
3851
  if (!token) {
3624
- console.log(chalk5.yellow("Please log in first: xploitscan auth login"));
3852
+ console.log(chalk6.yellow("Please log in first: xploitscan auth login"));
3625
3853
  return;
3626
3854
  }
3627
- console.log(chalk5.cyan("Creating checkout session..."));
3855
+ console.log(chalk6.cyan("Creating checkout session..."));
3628
3856
  const url = await getCheckoutUrl();
3629
3857
  if (url) {
3630
- console.log(chalk5.green(`
3858
+ console.log(chalk6.green(`
3631
3859
  Open this URL to upgrade:`));
3632
- console.log(chalk5.bold.underline(url));
3860
+ console.log(chalk6.bold.underline(url));
3633
3861
  const { execFile: execFile4 } = await import("child_process");
3634
3862
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3635
3863
  execFile4(openCmd, [url], () => {
3636
3864
  });
3637
3865
  } else {
3638
- 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."));
3639
3867
  }
3640
3868
  });
3641
3869
  program.parse();