xploitscan 1.1.11 → 1.2.1
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 +29 -5
- package/dist/{api-GHG3FCFB.js → api-MFCSQQ2Y.js} +2 -2
- package/dist/{chunk-HYXMH2H6.js → chunk-4OFR3SIS.js} +388 -26
- package/dist/{chunk-HYXMH2H6.js.map → chunk-4OFR3SIS.js.map} +1 -1
- package/dist/index.js +146 -57
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- /package/dist/{api-GHG3FCFB.js.map → api-MFCSQQ2Y.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
clearProRulesCache,
|
|
8
8
|
clearToken,
|
|
9
9
|
detectFramework,
|
|
10
|
+
detectPlatform,
|
|
10
11
|
downloadProRulesBundle,
|
|
11
12
|
filterFalsePositives,
|
|
12
13
|
getStoredToken,
|
|
@@ -18,22 +19,73 @@ import {
|
|
|
18
19
|
storeToken,
|
|
19
20
|
syncUser,
|
|
20
21
|
uploadScanResults
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-4OFR3SIS.js";
|
|
22
23
|
|
|
23
24
|
// src/index.ts
|
|
24
25
|
import { Command } from "commander";
|
|
25
26
|
|
|
26
27
|
// src/commands/scan.ts
|
|
27
|
-
import { resolve as resolve2, join as
|
|
28
|
-
import { watch as fsWatch } from "fs";
|
|
28
|
+
import { resolve as resolve2, join as join6 } from "path";
|
|
29
|
+
import { watch as fsWatch, existsSync as existsSync5 } from "fs";
|
|
29
30
|
import ora from "ora";
|
|
30
31
|
import chalk2 from "chalk";
|
|
31
32
|
|
|
32
33
|
// src/utils/files.ts
|
|
33
34
|
import fg from "fast-glob";
|
|
35
|
+
import ignore2 from "ignore";
|
|
36
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
37
|
+
import { join as join2, resolve } from "path";
|
|
38
|
+
|
|
39
|
+
// src/utils/ignore-rules.ts
|
|
34
40
|
import ignore from "ignore";
|
|
35
41
|
import { readFileSync, existsSync } from "fs";
|
|
36
|
-
import { join
|
|
42
|
+
import { join } from "path";
|
|
43
|
+
var RULE_TOKEN = "(?:VC\\d+|scanner)";
|
|
44
|
+
var RULE_SCOPED_RE = new RegExp(
|
|
45
|
+
`^\\s*([^#].*?\\S)\\s+(${RULE_TOKEN}(?:\\s*,\\s*${RULE_TOKEN})*)\\s*$`,
|
|
46
|
+
"i"
|
|
47
|
+
);
|
|
48
|
+
function isRuleScopedLine(line) {
|
|
49
|
+
return RULE_SCOPED_RE.test(line);
|
|
50
|
+
}
|
|
51
|
+
function parseRuleIgnores(content) {
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const line of content.split("\n")) {
|
|
54
|
+
const m = line.match(RULE_SCOPED_RE);
|
|
55
|
+
if (!m) continue;
|
|
56
|
+
const glob = m[1].trim();
|
|
57
|
+
const rules = new Set(
|
|
58
|
+
m[2].split(",").map((r) => r.trim().toUpperCase())
|
|
59
|
+
);
|
|
60
|
+
entries.push({ matcher: ignore.default().add(glob), rules, raw: line.trim() });
|
|
61
|
+
}
|
|
62
|
+
return entries;
|
|
63
|
+
}
|
|
64
|
+
function loadRuleIgnores(dir) {
|
|
65
|
+
const p = join(dir, ".xploitscanignore");
|
|
66
|
+
if (!existsSync(p)) return [];
|
|
67
|
+
try {
|
|
68
|
+
return parseRuleIgnores(readFileSync(p, "utf-8"));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function applyRuleIgnores(findings, entries) {
|
|
74
|
+
if (entries.length === 0) return { kept: findings, suppressed: [] };
|
|
75
|
+
const kept = [];
|
|
76
|
+
const suppressed = [];
|
|
77
|
+
for (const f of findings) {
|
|
78
|
+
const rule = (f.rule ?? "").toUpperCase();
|
|
79
|
+
const hit = entries.some(
|
|
80
|
+
(e) => e.matcher.ignores(f.file) && (e.rules.has("SCANNER") || e.rules.has(rule))
|
|
81
|
+
);
|
|
82
|
+
if (hit) suppressed.push(f);
|
|
83
|
+
else kept.push(f);
|
|
84
|
+
}
|
|
85
|
+
return { kept, suppressed };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/utils/files.ts
|
|
37
89
|
var SOURCE_EXTENSIONS = [
|
|
38
90
|
"js",
|
|
39
91
|
"jsx",
|
|
@@ -165,15 +217,15 @@ var ALWAYS_IGNORE = [
|
|
|
165
217
|
"vulnerable-samples/**"
|
|
166
218
|
];
|
|
167
219
|
async function collectFiles(directory) {
|
|
168
|
-
const ig =
|
|
169
|
-
const gitignorePath =
|
|
170
|
-
if (
|
|
171
|
-
const gitignoreContent =
|
|
220
|
+
const ig = ignore2.default();
|
|
221
|
+
const gitignorePath = join2(directory, ".gitignore");
|
|
222
|
+
if (existsSync2(gitignorePath)) {
|
|
223
|
+
const gitignoreContent = readFileSync2(gitignorePath, "utf-8");
|
|
172
224
|
ig.add(gitignoreContent);
|
|
173
225
|
}
|
|
174
|
-
const xploitscanIgnorePath =
|
|
175
|
-
if (
|
|
176
|
-
const xploitscanIgnoreContent =
|
|
226
|
+
const xploitscanIgnorePath = join2(directory, ".xploitscanignore");
|
|
227
|
+
if (existsSync2(xploitscanIgnorePath)) {
|
|
228
|
+
const xploitscanIgnoreContent = readFileSync2(xploitscanIgnorePath, "utf-8").split("\n").filter((line) => !isRuleScopedLine(line)).join("\n");
|
|
177
229
|
ig.add(xploitscanIgnoreContent);
|
|
178
230
|
}
|
|
179
231
|
ig.add(ALWAYS_IGNORE);
|
|
@@ -193,11 +245,11 @@ async function collectFiles(directory) {
|
|
|
193
245
|
}
|
|
194
246
|
function readFileContents(directory, filePath) {
|
|
195
247
|
try {
|
|
196
|
-
const fullPath = resolve(
|
|
248
|
+
const fullPath = resolve(join2(directory, filePath));
|
|
197
249
|
if (!fullPath.startsWith(resolve(directory))) {
|
|
198
250
|
throw new Error("Path traversal detected");
|
|
199
251
|
}
|
|
200
|
-
return
|
|
252
|
+
return readFileSync2(fullPath, "utf-8");
|
|
201
253
|
} catch {
|
|
202
254
|
return null;
|
|
203
255
|
}
|
|
@@ -215,8 +267,8 @@ function getSnippet(content, line, contextLines = 2) {
|
|
|
215
267
|
|
|
216
268
|
// src/utils/config.ts
|
|
217
269
|
import { cosmiconfig } from "cosmiconfig";
|
|
218
|
-
import { readFileSync as
|
|
219
|
-
import { join as
|
|
270
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
271
|
+
import { join as join3 } from "path";
|
|
220
272
|
var defaults = {
|
|
221
273
|
exclude: [],
|
|
222
274
|
ai: true,
|
|
@@ -225,8 +277,8 @@ var defaults = {
|
|
|
225
277
|
};
|
|
226
278
|
function loadXploitscanRc(directory) {
|
|
227
279
|
try {
|
|
228
|
-
const rcPath =
|
|
229
|
-
const content =
|
|
280
|
+
const rcPath = join3(directory, ".xploitscanrc");
|
|
281
|
+
const content = readFileSync3(rcPath, "utf-8");
|
|
230
282
|
return JSON.parse(content);
|
|
231
283
|
} catch {
|
|
232
284
|
return null;
|
|
@@ -297,9 +349,9 @@ async function loadConfig(directory) {
|
|
|
297
349
|
|
|
298
350
|
// src/scanners/semgrep.ts
|
|
299
351
|
import { execFile } from "child_process";
|
|
300
|
-
import { existsSync as
|
|
352
|
+
import { existsSync as existsSync3 } from "fs";
|
|
301
353
|
import { readFile, mkdtemp, rm } from "fs/promises";
|
|
302
|
-
import { join as
|
|
354
|
+
import { join as join4 } from "path";
|
|
303
355
|
import { tmpdir } from "os";
|
|
304
356
|
var SEVERITY_MAP = {
|
|
305
357
|
error: "high",
|
|
@@ -323,8 +375,8 @@ async function runSemgrep(directory, customRulesDir) {
|
|
|
323
375
|
return { findings: [], available: false };
|
|
324
376
|
}
|
|
325
377
|
const findings = [];
|
|
326
|
-
const tmpDir = await mkdtemp(
|
|
327
|
-
const sarifPath =
|
|
378
|
+
const tmpDir = await mkdtemp(join4(tmpdir(), "xploitscan-semgrep-"));
|
|
379
|
+
const sarifPath = join4(tmpDir, "results.sarif");
|
|
328
380
|
try {
|
|
329
381
|
const args = [
|
|
330
382
|
"scan",
|
|
@@ -340,7 +392,7 @@ async function runSemgrep(directory, customRulesDir) {
|
|
|
340
392
|
"1000000"
|
|
341
393
|
];
|
|
342
394
|
args.push("--config", "auto");
|
|
343
|
-
if (customRulesDir &&
|
|
395
|
+
if (customRulesDir && existsSync3(customRulesDir)) {
|
|
344
396
|
args.push("--config", customRulesDir);
|
|
345
397
|
}
|
|
346
398
|
args.push(directory);
|
|
@@ -358,7 +410,7 @@ async function runSemgrep(directory, customRulesDir) {
|
|
|
358
410
|
}
|
|
359
411
|
);
|
|
360
412
|
});
|
|
361
|
-
if (!
|
|
413
|
+
if (!existsSync3(sarifPath)) return { findings, available: true };
|
|
362
414
|
const sarifContent = await readFile(sarifPath, "utf-8");
|
|
363
415
|
const sarif = JSON.parse(sarifContent);
|
|
364
416
|
for (const run of sarif.runs ?? []) {
|
|
@@ -416,8 +468,8 @@ function truncate(str, max) {
|
|
|
416
468
|
// src/scanners/gitleaks.ts
|
|
417
469
|
import { execFile as execFile2 } from "child_process";
|
|
418
470
|
import { readFile as readFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
419
|
-
import { existsSync as
|
|
420
|
-
import { join as
|
|
471
|
+
import { existsSync as existsSync4 } from "fs";
|
|
472
|
+
import { join as join5 } from "path";
|
|
421
473
|
import { tmpdir as tmpdir2 } from "os";
|
|
422
474
|
var RULE_SEVERITY = {
|
|
423
475
|
"aws-access-token": "critical",
|
|
@@ -451,8 +503,8 @@ async function runGitleaks(directory) {
|
|
|
451
503
|
return { findings: [], available: false };
|
|
452
504
|
}
|
|
453
505
|
const findings = [];
|
|
454
|
-
const tmpDir = await mkdtemp2(
|
|
455
|
-
const reportPath =
|
|
506
|
+
const tmpDir = await mkdtemp2(join5(tmpdir2(), "xploitscan-gitleaks-"));
|
|
507
|
+
const reportPath = join5(tmpDir, "results.json");
|
|
456
508
|
try {
|
|
457
509
|
const args = [
|
|
458
510
|
"detect",
|
|
@@ -482,7 +534,7 @@ async function runGitleaks(directory) {
|
|
|
482
534
|
}
|
|
483
535
|
);
|
|
484
536
|
});
|
|
485
|
-
if (!
|
|
537
|
+
if (!existsSync4(reportPath)) return { findings, available: true };
|
|
486
538
|
const reportContent = await readFile2(reportPath, "utf-8");
|
|
487
539
|
if (!reportContent.trim()) return { findings, available: true };
|
|
488
540
|
const results = JSON.parse(reportContent);
|
|
@@ -1718,6 +1770,21 @@ function renderTerminalReport(result, files) {
|
|
|
1718
1770
|
if (frameworks.length > 0 && frameworks[0] !== "unknown") {
|
|
1719
1771
|
console.log(chalk.gray(" Frameworks: ") + chalk.white(frameworks.join(", ")));
|
|
1720
1772
|
}
|
|
1773
|
+
const platforms = detectPlatform(files);
|
|
1774
|
+
if (platforms.length > 0) {
|
|
1775
|
+
const PLATFORM_LABELS = {
|
|
1776
|
+
lovable: "Lovable",
|
|
1777
|
+
bolt: "Bolt",
|
|
1778
|
+
replit: "Replit",
|
|
1779
|
+
cursor: "Cursor",
|
|
1780
|
+
v0: "v0",
|
|
1781
|
+
"claude-code": "Claude Code"
|
|
1782
|
+
};
|
|
1783
|
+
const labels = platforms.map((p) => PLATFORM_LABELS[p] ?? p);
|
|
1784
|
+
console.log(
|
|
1785
|
+
chalk.gray(" Detected: ") + chalk.cyan(labels.join(" + ") + " project") + chalk.gray(" \u2014 rules tuned for what these tools ship by default")
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1721
1788
|
}
|
|
1722
1789
|
const GRADE_PERCENTILES = { "A+": 98, "A": 90, "B": 70, "C": 45, "D": 20, "F": 5 };
|
|
1723
1790
|
const { grade, score, summary } = calculateGrade(findings, filesScanned);
|
|
@@ -2163,8 +2230,8 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
|
|
|
2163
2230
|
}
|
|
2164
2231
|
spinner.text = "Running external scanners...";
|
|
2165
2232
|
spinner.color = "yellow";
|
|
2166
|
-
const rulesDir = resolve2(
|
|
2167
|
-
const fallbackRulesDir = resolve2(
|
|
2233
|
+
const rulesDir = resolve2(join6(import.meta.dirname, "../../rules"));
|
|
2234
|
+
const fallbackRulesDir = resolve2(join6(dir, "../rules"));
|
|
2168
2235
|
const [semgrepResult, gitleaksResult] = await Promise.allSettled([
|
|
2169
2236
|
runSemgrep(dir, rulesDir).catch(() => runSemgrep(dir, fallbackRulesDir)).catch(() => ({ findings: [], available: false })),
|
|
2170
2237
|
runGitleaks(dir).catch(() => ({ findings: [], available: false }))
|
|
@@ -2252,12 +2319,23 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
|
|
|
2252
2319
|
}
|
|
2253
2320
|
spinner.stop();
|
|
2254
2321
|
const seen = /* @__PURE__ */ new Set();
|
|
2255
|
-
const
|
|
2322
|
+
const afterDedup = allFindings.filter((f) => {
|
|
2256
2323
|
const ruleKey = f.source === "gitleaks" ? `secret:${f.file}:${f.line}` : `${f.rule}:${f.file}:${f.line}`;
|
|
2257
2324
|
if (seen.has(ruleKey)) return false;
|
|
2258
2325
|
seen.add(ruleKey);
|
|
2259
2326
|
return true;
|
|
2260
2327
|
});
|
|
2328
|
+
const { kept: dedupedFindings, suppressed: ruleSuppressed } = applyRuleIgnores(
|
|
2329
|
+
afterDedup,
|
|
2330
|
+
loadRuleIgnores(dir)
|
|
2331
|
+
);
|
|
2332
|
+
if (verbose && ruleSuppressed.length > 0 && !isSilent) {
|
|
2333
|
+
console.log(
|
|
2334
|
+
chalk2.gray(
|
|
2335
|
+
` .xploitscanignore suppressed ${ruleSuppressed.length} finding${ruleSuppressed.length === 1 ? "" : "s"} by rule scope`
|
|
2336
|
+
)
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2261
2339
|
const result = {
|
|
2262
2340
|
findings: dedupedFindings,
|
|
2263
2341
|
filesScanned: files.length,
|
|
@@ -2265,6 +2343,17 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
|
|
|
2265
2343
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2266
2344
|
directory: dir
|
|
2267
2345
|
};
|
|
2346
|
+
const PLATFORM_MARKERS = [
|
|
2347
|
+
".cursorrules",
|
|
2348
|
+
".cursor/",
|
|
2349
|
+
".replit",
|
|
2350
|
+
"replit.nix",
|
|
2351
|
+
".bolt/",
|
|
2352
|
+
".stackblitzrc",
|
|
2353
|
+
"CLAUDE.md",
|
|
2354
|
+
".claude/"
|
|
2355
|
+
];
|
|
2356
|
+
const platformMarkerFiles = PLATFORM_MARKERS.filter((m) => existsSync5(join6(dir, m.replace(/\/$/, "")))).map((m) => ({ path: m, content: "" }));
|
|
2268
2357
|
switch (format) {
|
|
2269
2358
|
case "json":
|
|
2270
2359
|
renderJsonReport(result);
|
|
@@ -2282,7 +2371,7 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
|
|
|
2282
2371
|
renderDatadogReport(result);
|
|
2283
2372
|
break;
|
|
2284
2373
|
default:
|
|
2285
|
-
renderTerminalReport(result, fileContentsForAnalysis);
|
|
2374
|
+
renderTerminalReport(result, [...fileContentsForAnalysis, ...platformMarkerFiles]);
|
|
2286
2375
|
break;
|
|
2287
2376
|
}
|
|
2288
2377
|
if (aiReviewed && !isSilent) {
|
|
@@ -2511,13 +2600,13 @@ Open this URL in your browser to log in:`));
|
|
|
2511
2600
|
// src/commands/hook.ts
|
|
2512
2601
|
import { execSync } from "child_process";
|
|
2513
2602
|
import {
|
|
2514
|
-
existsSync as
|
|
2515
|
-
readFileSync as
|
|
2603
|
+
existsSync as existsSync6,
|
|
2604
|
+
readFileSync as readFileSync4,
|
|
2516
2605
|
writeFileSync,
|
|
2517
2606
|
chmodSync,
|
|
2518
2607
|
unlinkSync
|
|
2519
2608
|
} from "fs";
|
|
2520
|
-
import { join as
|
|
2609
|
+
import { join as join7 } from "path";
|
|
2521
2610
|
import chalk4 from "chalk";
|
|
2522
2611
|
var HOOK_START_MARKER = "# xploitscan-hook-start";
|
|
2523
2612
|
var HOOK_END_MARKER = "# xploitscan-hook-end";
|
|
@@ -2570,12 +2659,12 @@ async function installHookCommand(options) {
|
|
|
2570
2659
|
console.log(chalk4.gray(" Run this command from inside a git repo."));
|
|
2571
2660
|
process.exit(1);
|
|
2572
2661
|
}
|
|
2573
|
-
const hooksDir =
|
|
2574
|
-
const hookPath =
|
|
2662
|
+
const hooksDir = join7(gitRoot, ".git", "hooks");
|
|
2663
|
+
const hookPath = join7(hooksDir, "pre-commit");
|
|
2575
2664
|
let existingContent = "";
|
|
2576
2665
|
let hadExisting = false;
|
|
2577
|
-
if (
|
|
2578
|
-
existingContent =
|
|
2666
|
+
if (existsSync6(hookPath)) {
|
|
2667
|
+
existingContent = readFileSync4(hookPath, "utf-8");
|
|
2579
2668
|
hadExisting = true;
|
|
2580
2669
|
const alreadyInstalled = existingContent.includes(HOOK_START_MARKER) && existingContent.includes(HOOK_END_MARKER);
|
|
2581
2670
|
if (alreadyInstalled && !options.force) {
|
|
@@ -2620,12 +2709,12 @@ async function uninstallHookCommand() {
|
|
|
2620
2709
|
console.log(chalk4.gray(" Run this command from inside a git repo."));
|
|
2621
2710
|
process.exit(1);
|
|
2622
2711
|
}
|
|
2623
|
-
const hookPath =
|
|
2624
|
-
if (!
|
|
2712
|
+
const hookPath = join7(gitRoot, ".git", "hooks", "pre-commit");
|
|
2713
|
+
if (!existsSync6(hookPath)) {
|
|
2625
2714
|
console.log(chalk4.yellow("\u26A0 No pre-commit hook found. Nothing to uninstall."));
|
|
2626
2715
|
return;
|
|
2627
2716
|
}
|
|
2628
|
-
const content =
|
|
2717
|
+
const content = readFileSync4(hookPath, "utf-8");
|
|
2629
2718
|
if (!content.includes(HOOK_START_MARKER)) {
|
|
2630
2719
|
console.log(chalk4.yellow("\u26A0 XploitScan hook not found in pre-commit file."));
|
|
2631
2720
|
console.log(chalk4.gray(" Nothing to remove."));
|
|
@@ -2651,28 +2740,28 @@ async function uninstallHookCommand() {
|
|
|
2651
2740
|
|
|
2652
2741
|
// src/commands/cursor.ts
|
|
2653
2742
|
import chalk5 from "chalk";
|
|
2654
|
-
import { mkdirSync, existsSync as
|
|
2655
|
-
import { join as
|
|
2743
|
+
import { mkdirSync, existsSync as existsSync7, writeFileSync as writeFileSync2, readFileSync as readFileSync5 } from "fs";
|
|
2744
|
+
import { join as join8, dirname } from "path";
|
|
2656
2745
|
import { fileURLToPath } from "url";
|
|
2657
2746
|
async function cursorInstallCommand(opts = {}) {
|
|
2658
2747
|
const cwd = process.cwd();
|
|
2659
2748
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2660
2749
|
const candidates = [
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2750
|
+
join8(here, "templates"),
|
|
2751
|
+
join8(here, "..", "templates"),
|
|
2752
|
+
join8(here, "..", "src", "templates")
|
|
2664
2753
|
];
|
|
2665
|
-
const templatesDir = candidates.find((p) =>
|
|
2754
|
+
const templatesDir = candidates.find((p) => existsSync7(join8(p, "cursor-security.mdc")));
|
|
2666
2755
|
if (!templatesDir) {
|
|
2667
2756
|
console.error(chalk5.red("Could not locate XploitScan rule templates. Try reinstalling: npm i -g xploitscan@latest"));
|
|
2668
2757
|
process.exit(1);
|
|
2669
2758
|
}
|
|
2670
|
-
const mdcSrc =
|
|
2671
|
-
const legacySrc =
|
|
2759
|
+
const mdcSrc = readFileSync5(join8(templatesDir, "cursor-security.mdc"), "utf-8");
|
|
2760
|
+
const legacySrc = readFileSync5(join8(templatesDir, "cursorrules-legacy.txt"), "utf-8");
|
|
2672
2761
|
if (!opts.legacyOnly) {
|
|
2673
|
-
const mdcDir =
|
|
2674
|
-
const mdcPath =
|
|
2675
|
-
if (
|
|
2762
|
+
const mdcDir = join8(cwd, ".cursor", "rules");
|
|
2763
|
+
const mdcPath = join8(mdcDir, "xploitscan-security.mdc");
|
|
2764
|
+
if (existsSync7(mdcPath) && !opts.force) {
|
|
2676
2765
|
console.log(chalk5.yellow(`Skipping ${mdcPath} (already exists, use --force to overwrite)`));
|
|
2677
2766
|
} else {
|
|
2678
2767
|
mkdirSync(mdcDir, { recursive: true });
|
|
@@ -2680,8 +2769,8 @@ async function cursorInstallCommand(opts = {}) {
|
|
|
2680
2769
|
console.log(chalk5.green(`Installed ${mdcPath}`));
|
|
2681
2770
|
}
|
|
2682
2771
|
}
|
|
2683
|
-
const legacyPath =
|
|
2684
|
-
if (
|
|
2772
|
+
const legacyPath = join8(cwd, ".cursorrules");
|
|
2773
|
+
if (existsSync7(legacyPath) && !opts.force) {
|
|
2685
2774
|
console.log(chalk5.yellow(`Skipping ${legacyPath} (already exists, use --force to overwrite)`));
|
|
2686
2775
|
} else {
|
|
2687
2776
|
writeFileSync2(legacyPath, legacySrc);
|
|
@@ -2696,7 +2785,7 @@ async function cursorInstallCommand(opts = {}) {
|
|
|
2696
2785
|
var program = new Command();
|
|
2697
2786
|
program.name("xploitscan").description(
|
|
2698
2787
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
2699
|
-
).version("1.1
|
|
2788
|
+
).version("1.2.1");
|
|
2700
2789
|
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option(
|
|
2701
2790
|
"-f, --format <format>",
|
|
2702
2791
|
"Output format: terminal, json, sarif, splunk-hec, elastic-ecs, datadog-logs",
|
|
@@ -2725,7 +2814,7 @@ cursor.command("install").description("Drop XploitScan security rules into .curs
|
|
|
2725
2814
|
await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });
|
|
2726
2815
|
});
|
|
2727
2816
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
2728
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
2817
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-MFCSQQ2Y.js");
|
|
2729
2818
|
const chalk6 = (await import("chalk")).default;
|
|
2730
2819
|
const token = getStoredToken2();
|
|
2731
2820
|
if (!token) {
|