xploitscan 1.3.0 → 1.3.2
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 +8 -0
- package/dist/{api-AX6R4QAD.js → api-6FD27EJY.js} +2 -2
- package/dist/{chunk-LWX7UPO5.js → chunk-2VDAYFPC.js} +12 -1
- package/dist/{chunk-LWX7UPO5.js.map → chunk-2VDAYFPC.js.map} +1 -1
- package/dist/index.js +94 -36
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- /package/dist/{api-AX6R4QAD.js.map → api-6FD27EJY.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -21,13 +21,13 @@ import {
|
|
|
21
21
|
syncUser,
|
|
22
22
|
uploadScanResults,
|
|
23
23
|
vendoredReason
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-2VDAYFPC.js";
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
import { Command } from "commander";
|
|
28
28
|
|
|
29
29
|
// src/commands/scan.ts
|
|
30
|
-
import { resolve as
|
|
30
|
+
import { resolve as resolve3, join as join6 } from "path";
|
|
31
31
|
import { watch as fsWatch, existsSync as existsSync5 } from "fs";
|
|
32
32
|
import ora from "ora";
|
|
33
33
|
import chalk2 from "chalk";
|
|
@@ -36,12 +36,54 @@ import chalk2 from "chalk";
|
|
|
36
36
|
import fg from "fast-glob";
|
|
37
37
|
import ignore2 from "ignore";
|
|
38
38
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
39
|
-
import { join as join2, resolve } from "path";
|
|
39
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
40
40
|
|
|
41
41
|
// src/utils/ignore-rules.ts
|
|
42
42
|
import ignore from "ignore";
|
|
43
|
-
import { readFileSync
|
|
44
|
-
|
|
43
|
+
import { readFileSync } from "fs";
|
|
44
|
+
|
|
45
|
+
// src/utils/ignore-discovery.ts
|
|
46
|
+
import { existsSync } from "fs";
|
|
47
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
48
|
+
function discoverIgnoreFiles(directory) {
|
|
49
|
+
const found = [];
|
|
50
|
+
let dir = resolve(directory);
|
|
51
|
+
for (; ; ) {
|
|
52
|
+
const file = join(dir, ".xploitscanignore");
|
|
53
|
+
if (existsSync(file)) found.push({ file, dir });
|
|
54
|
+
if (existsSync(join(dir, ".git"))) break;
|
|
55
|
+
const parent = dirname(dir);
|
|
56
|
+
if (parent === dir) break;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return found.reverse();
|
|
60
|
+
}
|
|
61
|
+
function rebasePattern(pattern, fromDir, toDir) {
|
|
62
|
+
const trimmed = pattern.trim();
|
|
63
|
+
if (trimmed === "" || trimmed.startsWith("#")) return pattern;
|
|
64
|
+
const from = resolve(fromDir);
|
|
65
|
+
const to = resolve(toDir);
|
|
66
|
+
if (from === to) return pattern;
|
|
67
|
+
const rel = relative(from, to).split(sep).join("/");
|
|
68
|
+
if (rel === "" || rel.startsWith("..")) return pattern;
|
|
69
|
+
const negated = trimmed.startsWith("!");
|
|
70
|
+
const body = negated ? trimmed.slice(1) : trimmed;
|
|
71
|
+
const anchored = body.startsWith("/");
|
|
72
|
+
const bare = anchored ? body.slice(1) : body;
|
|
73
|
+
const withoutTrailing = bare.endsWith("/") ? bare.slice(0, -1) : bare;
|
|
74
|
+
if (!anchored && !withoutTrailing.includes("/")) return pattern;
|
|
75
|
+
const keep = (p) => negated ? `!${p}` : p;
|
|
76
|
+
if (withoutTrailing === rel || rel.startsWith(`${withoutTrailing}/`)) {
|
|
77
|
+
return keep("**");
|
|
78
|
+
}
|
|
79
|
+
if (bare.startsWith(`${rel}/`)) return keep(bare.slice(rel.length + 1));
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function rebaseContent(content, fromDir, toDir) {
|
|
83
|
+
return content.split("\n").map((line) => rebasePattern(line, fromDir, toDir)).filter((line) => line !== null).join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/utils/ignore-rules.ts
|
|
45
87
|
var RULE_TOKEN = "(?:VC\\d+|scanner)";
|
|
46
88
|
var RULE_SCOPED_RE = new RegExp(
|
|
47
89
|
`^\\s*([^#].*?\\S)\\s+(${RULE_TOKEN}(?:\\s*,\\s*${RULE_TOKEN})*)\\s*$`,
|
|
@@ -50,12 +92,17 @@ var RULE_SCOPED_RE = new RegExp(
|
|
|
50
92
|
function isRuleScopedLine(line) {
|
|
51
93
|
return RULE_SCOPED_RE.test(line);
|
|
52
94
|
}
|
|
53
|
-
function parseRuleIgnores(content) {
|
|
95
|
+
function parseRuleIgnores(content, fromDir, toDir) {
|
|
54
96
|
const entries = [];
|
|
55
97
|
for (const line of content.split("\n")) {
|
|
56
98
|
const m = line.match(RULE_SCOPED_RE);
|
|
57
99
|
if (!m) continue;
|
|
58
|
-
|
|
100
|
+
let glob = m[1].trim();
|
|
101
|
+
if (fromDir && toDir) {
|
|
102
|
+
const rebased = rebasePattern(glob, fromDir, toDir);
|
|
103
|
+
if (rebased === null) continue;
|
|
104
|
+
glob = rebased;
|
|
105
|
+
}
|
|
59
106
|
const rules = new Set(
|
|
60
107
|
m[2].split(",").map((r) => r.trim().toUpperCase())
|
|
61
108
|
);
|
|
@@ -64,13 +111,14 @@ function parseRuleIgnores(content) {
|
|
|
64
111
|
return entries;
|
|
65
112
|
}
|
|
66
113
|
function loadRuleIgnores(dir) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
const entries = [];
|
|
115
|
+
for (const { file, dir: ignoreDir } of discoverIgnoreFiles(dir)) {
|
|
116
|
+
try {
|
|
117
|
+
entries.push(...parseRuleIgnores(readFileSync(file, "utf-8"), ignoreDir, dir));
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
73
120
|
}
|
|
121
|
+
return entries;
|
|
74
122
|
}
|
|
75
123
|
function applyRuleIgnores(findings, entries) {
|
|
76
124
|
if (entries.length === 0) return { kept: findings, suppressed: [] };
|
|
@@ -225,10 +273,12 @@ async function collectFiles(directory) {
|
|
|
225
273
|
const gitignoreContent = readFileSync2(gitignorePath, "utf-8");
|
|
226
274
|
ig.add(gitignoreContent);
|
|
227
275
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
276
|
+
for (const { file, dir } of discoverIgnoreFiles(directory)) {
|
|
277
|
+
try {
|
|
278
|
+
const content = readFileSync2(file, "utf-8").split("\n").filter((line) => !isRuleScopedLine(line)).join("\n");
|
|
279
|
+
ig.add(rebaseContent(content, dir, directory));
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
232
282
|
}
|
|
233
283
|
ig.add(ALWAYS_IGNORE);
|
|
234
284
|
const patterns = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);
|
|
@@ -247,8 +297,8 @@ async function collectFiles(directory) {
|
|
|
247
297
|
}
|
|
248
298
|
function readFileContents(directory, filePath) {
|
|
249
299
|
try {
|
|
250
|
-
const fullPath =
|
|
251
|
-
if (!fullPath.startsWith(
|
|
300
|
+
const fullPath = resolve2(join2(directory, filePath));
|
|
301
|
+
if (!fullPath.startsWith(resolve2(directory))) {
|
|
252
302
|
throw new Error("Path traversal detected");
|
|
253
303
|
}
|
|
254
304
|
return readFileSync2(fullPath, "utf-8");
|
|
@@ -368,9 +418,9 @@ function semgrepSeverityToXploitscan(level) {
|
|
|
368
418
|
return SEVERITY_MAP[level] ?? "medium";
|
|
369
419
|
}
|
|
370
420
|
async function isSemgrepInstalled() {
|
|
371
|
-
return new Promise((
|
|
421
|
+
return new Promise((resolve4) => {
|
|
372
422
|
execFile("semgrep", ["--version"], (error) => {
|
|
373
|
-
|
|
423
|
+
resolve4(!error);
|
|
374
424
|
});
|
|
375
425
|
});
|
|
376
426
|
}
|
|
@@ -401,7 +451,7 @@ async function runSemgrep(directory, customRulesDir) {
|
|
|
401
451
|
args.push("--config", customRulesDir);
|
|
402
452
|
}
|
|
403
453
|
args.push(directory);
|
|
404
|
-
await new Promise((
|
|
454
|
+
await new Promise((resolve4, reject) => {
|
|
405
455
|
const proc = execFile(
|
|
406
456
|
"semgrep",
|
|
407
457
|
args,
|
|
@@ -410,7 +460,7 @@ async function runSemgrep(directory, customRulesDir) {
|
|
|
410
460
|
if (error && error.code !== 1) {
|
|
411
461
|
reject(new Error(`Semgrep failed: ${stderr || error.message}`));
|
|
412
462
|
} else {
|
|
413
|
-
|
|
463
|
+
resolve4();
|
|
414
464
|
}
|
|
415
465
|
}
|
|
416
466
|
);
|
|
@@ -496,9 +546,9 @@ var RULE_SEVERITY = {
|
|
|
496
546
|
"firebase-api-key": "high"
|
|
497
547
|
};
|
|
498
548
|
async function isGitleaksInstalled() {
|
|
499
|
-
return new Promise((
|
|
549
|
+
return new Promise((resolve4) => {
|
|
500
550
|
execFile2("gitleaks", ["version"], (error) => {
|
|
501
|
-
|
|
551
|
+
resolve4(!error);
|
|
502
552
|
});
|
|
503
553
|
});
|
|
504
554
|
}
|
|
@@ -524,7 +574,7 @@ async function runGitleaks(directory) {
|
|
|
524
574
|
"0"
|
|
525
575
|
// Don't fail on findings
|
|
526
576
|
];
|
|
527
|
-
await new Promise((
|
|
577
|
+
await new Promise((resolve4, reject) => {
|
|
528
578
|
execFile2(
|
|
529
579
|
"gitleaks",
|
|
530
580
|
args,
|
|
@@ -534,7 +584,7 @@ async function runGitleaks(directory) {
|
|
|
534
584
|
const sanitizedError = (stderr || error.message).slice(0, 200);
|
|
535
585
|
reject(new Error(`Gitleaks failed: ${sanitizedError}`));
|
|
536
586
|
} else {
|
|
537
|
-
|
|
587
|
+
resolve4();
|
|
538
588
|
}
|
|
539
589
|
}
|
|
540
590
|
);
|
|
@@ -627,6 +677,14 @@ ${existingFindings.map((f) => `- ${f.file}:${f.line} \u2014 ${f.title}`).join("\
|
|
|
627
677
|
const response = await client.messages.create({
|
|
628
678
|
model: "claude-sonnet-4-5-20250514",
|
|
629
679
|
max_tokens: 4096,
|
|
680
|
+
// Pinned for the same reason as the FP filter, and the stakes are
|
|
681
|
+
// higher here: findings returned by this analyzer are pushed onto
|
|
682
|
+
// allFindings in commands/scan.ts, which is what calculateGrade()
|
|
683
|
+
// scores. Left at the API default of 1.0, a user could scan the same
|
|
684
|
+
// unchanged commit twice and get a different letter grade — with
|
|
685
|
+
// nothing in the output to explain why. Discovery arguably benefits
|
|
686
|
+
// from sampling variety; a grade the user is asked to trust does not.
|
|
687
|
+
temperature: 0,
|
|
630
688
|
messages: [
|
|
631
689
|
{
|
|
632
690
|
role: "user",
|
|
@@ -2091,7 +2149,7 @@ function renderDatadogReport(result) {
|
|
|
2091
2149
|
|
|
2092
2150
|
// src/commands/scan.ts
|
|
2093
2151
|
async function scanCommand(directory, options) {
|
|
2094
|
-
const dir =
|
|
2152
|
+
const dir = resolve3(directory || ".");
|
|
2095
2153
|
const format = options.format ?? "terminal";
|
|
2096
2154
|
const verbose = options.verbose ?? false;
|
|
2097
2155
|
const startTime = Date.now();
|
|
@@ -2263,8 +2321,8 @@ ${isMonthlyCap ? "Monthly" : "Daily"} scan limit reached.`));
|
|
|
2263
2321
|
}
|
|
2264
2322
|
spinner.text = "Running external scanners...";
|
|
2265
2323
|
spinner.color = "yellow";
|
|
2266
|
-
const rulesDir =
|
|
2267
|
-
const fallbackRulesDir =
|
|
2324
|
+
const rulesDir = resolve3(join6(import.meta.dirname, "../../rules"));
|
|
2325
|
+
const fallbackRulesDir = resolve3(join6(dir, "../rules"));
|
|
2268
2326
|
const [semgrepResult, gitleaksResult] = await Promise.allSettled([
|
|
2269
2327
|
runSemgrep(dir, rulesDir).catch(() => runSemgrep(dir, fallbackRulesDir)).catch(() => ({ findings: [], available: false })),
|
|
2270
2328
|
runGitleaks(dir).catch(() => ({ findings: [], available: false }))
|
|
@@ -2554,7 +2612,7 @@ async function whoamiCommand() {
|
|
|
2554
2612
|
}
|
|
2555
2613
|
}
|
|
2556
2614
|
async function waitForBrowserLogin() {
|
|
2557
|
-
return new Promise((
|
|
2615
|
+
return new Promise((resolve4, reject) => {
|
|
2558
2616
|
const expectedState = randomUUID();
|
|
2559
2617
|
const server = createServer((req, res) => {
|
|
2560
2618
|
if (!req.url) {
|
|
@@ -2587,7 +2645,7 @@ async function waitForBrowserLogin() {
|
|
|
2587
2645
|
</html>
|
|
2588
2646
|
`);
|
|
2589
2647
|
server.close();
|
|
2590
|
-
|
|
2648
|
+
resolve4({ token, email, userId });
|
|
2591
2649
|
} else {
|
|
2592
2650
|
res.writeHead(400);
|
|
2593
2651
|
res.end("Missing parameters");
|
|
@@ -2782,11 +2840,11 @@ async function uninstallHookCommand() {
|
|
|
2782
2840
|
// src/commands/cursor.ts
|
|
2783
2841
|
import chalk5 from "chalk";
|
|
2784
2842
|
import { mkdirSync, existsSync as existsSync7, writeFileSync as writeFileSync2, readFileSync as readFileSync5 } from "fs";
|
|
2785
|
-
import { join as join8, dirname } from "path";
|
|
2843
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
2786
2844
|
import { fileURLToPath } from "url";
|
|
2787
2845
|
async function cursorInstallCommand(opts = {}) {
|
|
2788
2846
|
const cwd = process.cwd();
|
|
2789
|
-
const here =
|
|
2847
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
2790
2848
|
const candidates = [
|
|
2791
2849
|
join8(here, "templates"),
|
|
2792
2850
|
join8(here, "..", "templates"),
|
|
@@ -2826,7 +2884,7 @@ async function cursorInstallCommand(opts = {}) {
|
|
|
2826
2884
|
var program = new Command();
|
|
2827
2885
|
program.name("xploitscan").description(
|
|
2828
2886
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
2829
|
-
).version("1.3.
|
|
2887
|
+
).version("1.3.2");
|
|
2830
2888
|
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option(
|
|
2831
2889
|
"-f, --format <format>",
|
|
2832
2890
|
"Output format: terminal, json, sarif, splunk-hec, elastic-ecs, datadog-logs",
|
|
@@ -2859,7 +2917,7 @@ cursor.command("install").description("Drop XploitScan security rules into .curs
|
|
|
2859
2917
|
await cursorInstallCommand({ force: opts.force, legacyOnly: opts.legacyOnly });
|
|
2860
2918
|
});
|
|
2861
2919
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
2862
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
2920
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-6FD27EJY.js");
|
|
2863
2921
|
const chalk6 = (await import("chalk")).default;
|
|
2864
2922
|
const token = getStoredToken2();
|
|
2865
2923
|
if (!token) {
|