xploitscan 1.0.4 → 1.0.8
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 +314 -54
- 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
|
@@ -335,15 +335,20 @@ var hardcodedSecrets = {
|
|
|
335
335
|
// Database URLs with credentials
|
|
336
336
|
/(?:postgres|mysql|mongodb(?:\+srv)?):\/\/[^:]+:[^@]+@[^/\s"'`]+/gi
|
|
337
337
|
];
|
|
338
|
+
const fixMessages = [
|
|
339
|
+
"Move this API key to an environment variable. If this key has been committed, rotate it immediately \u2014 it may have already been scraped by bots.",
|
|
340
|
+
"AWS access key detected \u2014 may grant full account access (EC2, S3, IAM, billing). Rotate immediately in AWS Console \u2192 IAM \u2192 Security Credentials. Use IAM roles or environment variables instead.",
|
|
341
|
+
"Stripe key detected. sk_live_ keys can process real charges, issue refunds, and access customer payment data. sk_test_ keys are lower risk but should still not be committed. Rotate in Stripe Dashboard \u2192 Developers \u2192 API Keys.",
|
|
342
|
+
"Supabase key detected. Service role keys bypass Row Level Security and grant full database read/write access. Move to a server-side environment variable immediately.",
|
|
343
|
+
"OpenAI API key detected \u2014 grants full API access and can incur charges. Rotate at platform.openai.com \u2192 API Keys.",
|
|
344
|
+
"Hardcoded token or password detected. Move to an environment variable and rotate the credential if it has been committed to version control.",
|
|
345
|
+
"Private key found in source code. If this has been committed to version control, consider the key compromised \u2014 generate a new key pair and revoke the old one.",
|
|
346
|
+
"Database credentials in connection string. An attacker with this URL has full database access. Move to an environment variable, restrict network access, and rotate the password."
|
|
347
|
+
];
|
|
338
348
|
const matches = [];
|
|
339
|
-
for (
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
pattern,
|
|
343
|
-
hardcodedSecrets,
|
|
344
|
-
filePath,
|
|
345
|
-
() => "Move this secret to an environment variable and add it to .env (not committed to git). Use .env.example to document the required variables."
|
|
346
|
-
);
|
|
349
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
350
|
+
const pattern = patterns[pi];
|
|
351
|
+
const rawMatches = findMatches(content, pattern, hardcodedSecrets, filePath, () => fixMessages[pi]);
|
|
347
352
|
for (const rm3 of rawMatches) {
|
|
348
353
|
const lineText = content.split("\n")[rm3.line - 1] || "";
|
|
349
354
|
const trimmed = lineText.trimStart();
|
|
@@ -1144,51 +1149,52 @@ function detectFramework(files) {
|
|
|
1144
1149
|
if (frameworks.size === 0) frameworks.add("unknown");
|
|
1145
1150
|
return [...frameworks];
|
|
1146
1151
|
}
|
|
1147
|
-
function calculateGrade(findings,
|
|
1152
|
+
function calculateGrade(findings, _totalFiles) {
|
|
1148
1153
|
if (findings.length === 0) {
|
|
1149
|
-
return { grade: "A+", score: 100, summary: "No security issues detected. Excellent
|
|
1154
|
+
return { grade: "A+", score: 100, summary: "No security issues detected. Excellent." };
|
|
1150
1155
|
}
|
|
1151
|
-
let
|
|
1156
|
+
let critical = 0, high = 0, medium = 0, low = 0;
|
|
1152
1157
|
for (const f of findings) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
case "medium":
|
|
1161
|
-
deductions += 4;
|
|
1162
|
-
break;
|
|
1163
|
-
case "low":
|
|
1164
|
-
deductions += 1;
|
|
1165
|
-
break;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
const sizeBuffer = Math.min(Math.log2(Math.max(totalFiles, 1)) * 2, 15);
|
|
1169
|
-
const score = Math.max(0, Math.min(100, 100 - deductions + sizeBuffer));
|
|
1158
|
+
if (f.severity === "critical") critical++;
|
|
1159
|
+
else if (f.severity === "high") high++;
|
|
1160
|
+
else if (f.severity === "medium") medium++;
|
|
1161
|
+
else if (f.severity === "low") low++;
|
|
1162
|
+
}
|
|
1163
|
+
const deductions = critical * 15 + high * 7 + medium * 3 + low * 1;
|
|
1164
|
+
const rawScore = Math.max(0, 100 - deductions);
|
|
1170
1165
|
let grade;
|
|
1166
|
+
if (rawScore >= 97) grade = "A+";
|
|
1167
|
+
else if (rawScore >= 90) grade = "A";
|
|
1168
|
+
else if (rawScore >= 80) grade = "B";
|
|
1169
|
+
else if (rawScore >= 70) grade = "C";
|
|
1170
|
+
else if (rawScore >= 60) grade = "D";
|
|
1171
|
+
else grade = "F";
|
|
1172
|
+
const capGrade = (cap) => {
|
|
1173
|
+
const order = ["A+", "A", "B", "C", "D", "F"];
|
|
1174
|
+
return order.indexOf(grade) < order.indexOf(cap) ? cap : grade;
|
|
1175
|
+
};
|
|
1176
|
+
if (critical >= 1) grade = capGrade("D");
|
|
1177
|
+
else if (high >= 3) grade = capGrade("D");
|
|
1178
|
+
else if (high >= 1) grade = capGrade("B");
|
|
1179
|
+
else if (medium >= 5) grade = capGrade("B");
|
|
1180
|
+
else if (medium >= 1) grade = capGrade("A");
|
|
1171
1181
|
let summary;
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
summary =
|
|
1178
|
-
} else if (
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
summary =
|
|
1184
|
-
} else if (score >= 35) {
|
|
1185
|
-
grade = "D";
|
|
1186
|
-
summary = "Poor security \u2014 critical issues require immediate attention.";
|
|
1182
|
+
if (critical > 0) {
|
|
1183
|
+
summary = `${critical} critical ${critical === 1 ? "vulnerability" : "vulnerabilities"} require immediate attention.`;
|
|
1184
|
+
} else if (high >= 3) {
|
|
1185
|
+
summary = `${high} high-severity issues require urgent attention.`;
|
|
1186
|
+
} else if (high > 0) {
|
|
1187
|
+
summary = `${high} high-severity ${high === 1 ? "issue needs" : "issues need"} attention.`;
|
|
1188
|
+
} else if (medium >= 5) {
|
|
1189
|
+
summary = `${medium} medium-severity issues to address.`;
|
|
1190
|
+
} else if (medium > 0) {
|
|
1191
|
+
summary = `Clean of critical and high issues. ${medium} medium-severity ${medium === 1 ? "issue" : "issues"} to review.`;
|
|
1192
|
+
} else if (low > 0) {
|
|
1193
|
+
summary = `Clean of critical, high, and medium issues. ${low} low-severity best-practice ${low === 1 ? "note" : "notes"}.`;
|
|
1187
1194
|
} else {
|
|
1188
|
-
|
|
1189
|
-
summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
|
|
1195
|
+
summary = "No security issues detected.";
|
|
1190
1196
|
}
|
|
1191
|
-
return { grade, score:
|
|
1197
|
+
return { grade, score: rawScore, summary };
|
|
1192
1198
|
}
|
|
1193
1199
|
var complianceMap = {
|
|
1194
1200
|
VC001: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
@@ -1321,7 +1327,29 @@ var complianceMap = {
|
|
|
1321
1327
|
VC128: { owasp: "A05:2021", cwe: "CWE-444" },
|
|
1322
1328
|
VC129: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
1323
1329
|
VC130: { owasp: "A07:2021", cwe: "CWE-307" },
|
|
1324
|
-
VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
|
|
1330
|
+
VC131: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
1331
|
+
// VC132–VC145: Service-specific API key detection
|
|
1332
|
+
VC132: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1333
|
+
VC133: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1334
|
+
VC134: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1335
|
+
VC135: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1336
|
+
VC136: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1337
|
+
VC137: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1338
|
+
VC138: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1339
|
+
VC139: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1340
|
+
VC140: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1341
|
+
VC141: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1342
|
+
VC142: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1343
|
+
VC143: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1344
|
+
VC144: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1345
|
+
VC145: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1346
|
+
// VC146–VC151: Secret exposure path analysis
|
|
1347
|
+
VC146: { owasp: "A07:2021", cwe: "CWE-598" },
|
|
1348
|
+
VC147: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
1349
|
+
VC148: { owasp: "A09:2021", cwe: "CWE-209" },
|
|
1350
|
+
VC149: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
1351
|
+
VC150: { owasp: "A07:2021", cwe: "CWE-615" },
|
|
1352
|
+
VC151: { owasp: "A07:2021", cwe: "CWE-214" }
|
|
1325
1353
|
};
|
|
1326
1354
|
var consoleLogProduction = {
|
|
1327
1355
|
id: "VC097",
|
|
@@ -1925,6 +1953,85 @@ function buildASTContext(content, filePath) {
|
|
|
1925
1953
|
};
|
|
1926
1954
|
}
|
|
1927
1955
|
|
|
1956
|
+
// src/scanners/osv.ts
|
|
1957
|
+
var OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
1958
|
+
var OSV_VULN_URL = "https://api.osv.dev/v1/vulns";
|
|
1959
|
+
var NETWORK_TIMEOUT_MS = 5e3;
|
|
1960
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
1961
|
+
const controller = new AbortController();
|
|
1962
|
+
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1963
|
+
try {
|
|
1964
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1965
|
+
} finally {
|
|
1966
|
+
clearTimeout(id);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
async function queryOsvBatch(queries) {
|
|
1970
|
+
if (queries.length === 0) return [];
|
|
1971
|
+
try {
|
|
1972
|
+
const batchRes = await fetchWithTimeout(
|
|
1973
|
+
OSV_BATCH_URL,
|
|
1974
|
+
{
|
|
1975
|
+
method: "POST",
|
|
1976
|
+
headers: { "Content-Type": "application/json" },
|
|
1977
|
+
body: JSON.stringify({ queries })
|
|
1978
|
+
},
|
|
1979
|
+
NETWORK_TIMEOUT_MS
|
|
1980
|
+
);
|
|
1981
|
+
if (!batchRes.ok) return [];
|
|
1982
|
+
const batch = await batchRes.json();
|
|
1983
|
+
if (!Array.isArray(batch.results)) return [];
|
|
1984
|
+
const uniqueVulnIds = /* @__PURE__ */ new Set();
|
|
1985
|
+
for (const result of batch.results) {
|
|
1986
|
+
for (const v of result.vulns ?? []) uniqueVulnIds.add(v.id);
|
|
1987
|
+
}
|
|
1988
|
+
const vulnDetails = /* @__PURE__ */ new Map();
|
|
1989
|
+
for (const vulnId of uniqueVulnIds) {
|
|
1990
|
+
try {
|
|
1991
|
+
const detailRes = await fetchWithTimeout(
|
|
1992
|
+
`${OSV_VULN_URL}/${encodeURIComponent(vulnId)}`,
|
|
1993
|
+
{ method: "GET" },
|
|
1994
|
+
NETWORK_TIMEOUT_MS
|
|
1995
|
+
);
|
|
1996
|
+
if (!detailRes.ok) continue;
|
|
1997
|
+
const detail = await detailRes.json();
|
|
1998
|
+
vulnDetails.set(vulnId, detail);
|
|
1999
|
+
} catch {
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const matches = [];
|
|
2003
|
+
batch.results.forEach((result, i) => {
|
|
2004
|
+
const ids = (result.vulns ?? []).map((v) => v.id);
|
|
2005
|
+
if (ids.length === 0) return;
|
|
2006
|
+
const q = queries[i];
|
|
2007
|
+
matches.push({
|
|
2008
|
+
ecosystem: q.package.ecosystem,
|
|
2009
|
+
name: q.package.name,
|
|
2010
|
+
version: q.version,
|
|
2011
|
+
vulns: ids.map((id) => vulnDetails.get(id) ?? { id })
|
|
2012
|
+
});
|
|
2013
|
+
});
|
|
2014
|
+
return matches;
|
|
2015
|
+
} catch {
|
|
2016
|
+
return [];
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
function osvSeverity(vuln) {
|
|
2020
|
+
const vectors = vuln.severity ?? [];
|
|
2021
|
+
for (const v of vectors) {
|
|
2022
|
+
const match = v.score.match(/\b(\d+(?:\.\d+)?)\b/);
|
|
2023
|
+
if (!match) continue;
|
|
2024
|
+
const score = parseFloat(match[1]);
|
|
2025
|
+
if (Number.isFinite(score)) {
|
|
2026
|
+
if (score >= 9) return "critical";
|
|
2027
|
+
if (score >= 7) return "high";
|
|
2028
|
+
if (score >= 4) return "medium";
|
|
2029
|
+
return "low";
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return "medium";
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1928
2035
|
// src/scanners/dependency-scanner.ts
|
|
1929
2036
|
var KNOWN_VULN_NPM = {
|
|
1930
2037
|
"lodash": [{
|
|
@@ -2225,6 +2332,94 @@ function scanDependencies(files) {
|
|
|
2225
2332
|
}
|
|
2226
2333
|
return findings;
|
|
2227
2334
|
}
|
|
2335
|
+
async function scanDependenciesOsv(files, alreadyFoundByRule) {
|
|
2336
|
+
const lookups = [];
|
|
2337
|
+
for (const { path: filePath, content } of files) {
|
|
2338
|
+
if (filePath.endsWith("package.json") && !filePath.includes("node_modules")) {
|
|
2339
|
+
try {
|
|
2340
|
+
const pkg = JSON.parse(content);
|
|
2341
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2342
|
+
for (const [name, rawVersion] of Object.entries(allDeps)) {
|
|
2343
|
+
const version = String(rawVersion).replace(/^[\^~>=<]*/g, "").trim();
|
|
2344
|
+
if (!version || version === "*" || version === "latest") continue;
|
|
2345
|
+
const line = content.split("\n").findIndex((l) => l.includes(`"${name}"`)) + 1;
|
|
2346
|
+
lookups.push({ ecosystem: "npm", name, version, file: filePath, line: line || 1, content });
|
|
2347
|
+
}
|
|
2348
|
+
} catch {
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (filePath.match(/requirements.*\.txt$/i)) {
|
|
2352
|
+
const lines = content.split("\n");
|
|
2353
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2354
|
+
const m = lines[i].trim().match(/^([a-zA-Z0-9_-]+)==([\d.]+)/);
|
|
2355
|
+
if (!m) continue;
|
|
2356
|
+
lookups.push({
|
|
2357
|
+
ecosystem: "PyPI",
|
|
2358
|
+
name: m[1].toLowerCase(),
|
|
2359
|
+
version: m[2],
|
|
2360
|
+
file: filePath,
|
|
2361
|
+
line: i + 1,
|
|
2362
|
+
content
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (filePath.endsWith("Gemfile.lock")) {
|
|
2367
|
+
const lines = content.split("\n");
|
|
2368
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2369
|
+
const m = lines[i].match(/^\s{4}([a-z0-9_-]+)\s+\(([\d.]+)\)/);
|
|
2370
|
+
if (!m) continue;
|
|
2371
|
+
lookups.push({
|
|
2372
|
+
ecosystem: "RubyGems",
|
|
2373
|
+
name: m[1],
|
|
2374
|
+
version: m[2],
|
|
2375
|
+
file: filePath,
|
|
2376
|
+
line: i + 1,
|
|
2377
|
+
content
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if (lookups.length === 0) return [];
|
|
2383
|
+
const CHUNK = 500;
|
|
2384
|
+
const findings = [];
|
|
2385
|
+
for (let start = 0; start < lookups.length; start += CHUNK) {
|
|
2386
|
+
const chunk = lookups.slice(start, start + CHUNK);
|
|
2387
|
+
const matches = await queryOsvBatch(
|
|
2388
|
+
chunk.map((l) => ({ package: { name: l.name, ecosystem: l.ecosystem }, version: l.version }))
|
|
2389
|
+
);
|
|
2390
|
+
const matchByKey = /* @__PURE__ */ new Map();
|
|
2391
|
+
for (const m of matches) matchByKey.set(`${m.ecosystem}:${m.name}:${m.version}`, m);
|
|
2392
|
+
for (const lookup of chunk) {
|
|
2393
|
+
const key = `${lookup.ecosystem}:${lookup.name}:${lookup.version}`;
|
|
2394
|
+
const match = matchByKey.get(key);
|
|
2395
|
+
if (!match) continue;
|
|
2396
|
+
for (const vuln of match.vulns) {
|
|
2397
|
+
const dedupeKey = `${lookup.name}:${vuln.id}`;
|
|
2398
|
+
if (alreadyFoundByRule.has(dedupeKey)) continue;
|
|
2399
|
+
alreadyFoundByRule.add(dedupeKey);
|
|
2400
|
+
const severity = osvSeverity(vuln);
|
|
2401
|
+
const title = vuln.summary || `${lookup.name} vulnerability ${vuln.id}`;
|
|
2402
|
+
const description = vuln.details || vuln.summary || `${lookup.name}@${lookup.version} is affected by ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id} for details.`;
|
|
2403
|
+
findings.push({
|
|
2404
|
+
id: `${vuln.id}-${lookup.file}:${lookup.line}`,
|
|
2405
|
+
rule: vuln.id,
|
|
2406
|
+
severity,
|
|
2407
|
+
title,
|
|
2408
|
+
description: description.slice(0, 500),
|
|
2409
|
+
file: lookup.file,
|
|
2410
|
+
line: lookup.line,
|
|
2411
|
+
snippet: getSnippet2(lookup.content, lookup.line),
|
|
2412
|
+
fix: `Upgrade ${lookup.name} to a version that resolves ${vuln.id}. See https://osv.dev/vulnerability/${vuln.id}`,
|
|
2413
|
+
category: "Dependencies",
|
|
2414
|
+
source: "custom",
|
|
2415
|
+
owasp: "A06:2021",
|
|
2416
|
+
cwe: "CWE-1395"
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return findings;
|
|
2422
|
+
}
|
|
2228
2423
|
|
|
2229
2424
|
// src/scanners/entropy-scanner.ts
|
|
2230
2425
|
function shannonEntropy(str) {
|
|
@@ -3136,6 +3331,24 @@ async function scanCommand(directory, options) {
|
|
|
3136
3331
|
f.confidence = "high";
|
|
3137
3332
|
}
|
|
3138
3333
|
allFindings.push(...depFindings);
|
|
3334
|
+
const dedupeKeys = /* @__PURE__ */ new Set();
|
|
3335
|
+
for (const f of depFindings) {
|
|
3336
|
+
const match = f.title.match(/^(\S+)/);
|
|
3337
|
+
if (match) dedupeKeys.add(`${match[1].toLowerCase()}:${f.rule}`);
|
|
3338
|
+
}
|
|
3339
|
+
spinner.text = "Checking dependencies against OSV.dev...";
|
|
3340
|
+
try {
|
|
3341
|
+
const osvFindings = await scanDependenciesOsv(fileContentsForAnalysis, dedupeKeys);
|
|
3342
|
+
for (const f of osvFindings) {
|
|
3343
|
+
f.confidence = "high";
|
|
3344
|
+
}
|
|
3345
|
+
allFindings.push(...osvFindings);
|
|
3346
|
+
if (verbose && osvFindings.length > 0) {
|
|
3347
|
+
spinner.info(`OSV.dev found ${osvFindings.length} additional vulnerable dependencies`);
|
|
3348
|
+
}
|
|
3349
|
+
} catch (err) {
|
|
3350
|
+
if (verbose) spinner.info(`OSV.dev lookup skipped: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
3351
|
+
}
|
|
3139
3352
|
if (verbose && depFindings.length > 0) {
|
|
3140
3353
|
spinner.info(`Dependency scanner found ${depFindings.length} issues`);
|
|
3141
3354
|
}
|
|
@@ -3588,11 +3801,54 @@ async function uninstallHookCommand() {
|
|
|
3588
3801
|
}
|
|
3589
3802
|
}
|
|
3590
3803
|
|
|
3804
|
+
// src/commands/cursor.ts
|
|
3805
|
+
import chalk5 from "chalk";
|
|
3806
|
+
import { mkdirSync, existsSync as existsSync5, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
3807
|
+
import { join as join7, dirname } from "path";
|
|
3808
|
+
import { fileURLToPath } from "url";
|
|
3809
|
+
async function cursorInstallCommand(opts = {}) {
|
|
3810
|
+
const cwd = process.cwd();
|
|
3811
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
3812
|
+
const candidates = [
|
|
3813
|
+
join7(here, "templates"),
|
|
3814
|
+
join7(here, "..", "templates"),
|
|
3815
|
+
join7(here, "..", "src", "templates")
|
|
3816
|
+
];
|
|
3817
|
+
const templatesDir = candidates.find((p) => existsSync5(join7(p, "cursor-security.mdc")));
|
|
3818
|
+
if (!templatesDir) {
|
|
3819
|
+
console.error(chalk5.red("Could not locate XploitScan rule templates. Try reinstalling: npm i -g xploitscan@latest"));
|
|
3820
|
+
process.exit(1);
|
|
3821
|
+
}
|
|
3822
|
+
const mdcSrc = readFileSync4(join7(templatesDir, "cursor-security.mdc"), "utf-8");
|
|
3823
|
+
const legacySrc = readFileSync4(join7(templatesDir, "cursorrules-legacy.txt"), "utf-8");
|
|
3824
|
+
if (!opts.legacyOnly) {
|
|
3825
|
+
const mdcDir = join7(cwd, ".cursor", "rules");
|
|
3826
|
+
const mdcPath = join7(mdcDir, "xploitscan-security.mdc");
|
|
3827
|
+
if (existsSync5(mdcPath) && !opts.force) {
|
|
3828
|
+
console.log(chalk5.yellow(`Skipping ${mdcPath} (already exists, use --force to overwrite)`));
|
|
3829
|
+
} else {
|
|
3830
|
+
mkdirSync(mdcDir, { recursive: true });
|
|
3831
|
+
writeFileSync2(mdcPath, mdcSrc);
|
|
3832
|
+
console.log(chalk5.green(`Installed ${mdcPath}`));
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
const legacyPath = join7(cwd, ".cursorrules");
|
|
3836
|
+
if (existsSync5(legacyPath) && !opts.force) {
|
|
3837
|
+
console.log(chalk5.yellow(`Skipping ${legacyPath} (already exists, use --force to overwrite)`));
|
|
3838
|
+
} else {
|
|
3839
|
+
writeFileSync2(legacyPath, legacySrc);
|
|
3840
|
+
console.log(chalk5.green(`Installed ${legacyPath}`));
|
|
3841
|
+
}
|
|
3842
|
+
console.log("");
|
|
3843
|
+
console.log(chalk5.cyan("Cursor will pick up these rules automatically the next time you open the project."));
|
|
3844
|
+
console.log(chalk5.gray("Run a scan any time to catch what slipped through: npx xploitscan scan ."));
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3591
3847
|
// src/index.ts
|
|
3592
3848
|
var program = new Command();
|
|
3593
3849
|
program.name("xploitscan").description(
|
|
3594
3850
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
3595
|
-
).version("1.0.
|
|
3851
|
+
).version("1.0.8");
|
|
3596
3852
|
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
3853
|
await scanCommand(directory, {
|
|
3598
3854
|
directory,
|
|
@@ -3612,26 +3868,30 @@ hook.command("install").description("Install a git pre-commit hook that runs Xpl
|
|
|
3612
3868
|
await installHookCommand({ force: opts.force });
|
|
3613
3869
|
});
|
|
3614
3870
|
hook.command("uninstall").description("Remove the XploitScan pre-commit hook").action(uninstallHookCommand);
|
|
3871
|
+
var cursor = program.command("cursor").description("Manage XploitScan integration with Cursor IDE");
|
|
3872
|
+
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) => {
|
|
3873
|
+
await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });
|
|
3874
|
+
});
|
|
3615
3875
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
3616
3876
|
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-HTHCG6QE.js");
|
|
3617
|
-
const
|
|
3877
|
+
const chalk6 = (await import("chalk")).default;
|
|
3618
3878
|
const token = getStoredToken2();
|
|
3619
3879
|
if (!token) {
|
|
3620
|
-
console.log(
|
|
3880
|
+
console.log(chalk6.yellow("Please log in first: xploitscan auth login"));
|
|
3621
3881
|
return;
|
|
3622
3882
|
}
|
|
3623
|
-
console.log(
|
|
3883
|
+
console.log(chalk6.cyan("Creating checkout session..."));
|
|
3624
3884
|
const url = await getCheckoutUrl();
|
|
3625
3885
|
if (url) {
|
|
3626
|
-
console.log(
|
|
3886
|
+
console.log(chalk6.green(`
|
|
3627
3887
|
Open this URL to upgrade:`));
|
|
3628
|
-
console.log(
|
|
3888
|
+
console.log(chalk6.bold.underline(url));
|
|
3629
3889
|
const { execFile: execFile4 } = await import("child_process");
|
|
3630
3890
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3631
3891
|
execFile4(openCmd, [url], () => {
|
|
3632
3892
|
});
|
|
3633
3893
|
} else {
|
|
3634
|
-
console.log(
|
|
3894
|
+
console.log(chalk6.red("Failed to create checkout session. Please try again."));
|
|
3635
3895
|
}
|
|
3636
3896
|
});
|
|
3637
3897
|
program.parse();
|