xploitscan 1.0.2 → 1.0.3
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/{api-ZNWEMMEL.js → api-HTHCG6QE.js} +2 -2
- package/dist/{chunk-IHRV7UHG.js → chunk-RJUUWD2F.js} +29 -6
- package/dist/chunk-RJUUWD2F.js.map +1 -0
- package/dist/index.js +164 -41
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-IHRV7UHG.js.map +0 -1
- /package/dist/{api-ZNWEMMEL.js.map → api-HTHCG6QE.js.map} +0 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
storeToken,
|
|
13
13
|
syncUser,
|
|
14
14
|
uploadScanResults
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-RJUUWD2F.js";
|
|
16
16
|
export {
|
|
17
17
|
checkUsage,
|
|
18
18
|
clearProRulesCache,
|
|
@@ -27,4 +27,4 @@ export {
|
|
|
27
27
|
syncUser,
|
|
28
28
|
uploadScanResults
|
|
29
29
|
};
|
|
30
|
-
//# sourceMappingURL=api-
|
|
30
|
+
//# sourceMappingURL=api-HTHCG6QE.js.map
|
|
@@ -10,6 +10,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
10
10
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
import { homedir } from "os";
|
|
13
|
+
import { createHash } from "crypto";
|
|
13
14
|
var CONFIG_DIR = join(homedir(), ".xploitscan");
|
|
14
15
|
var TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
15
16
|
var PRO_RULES_FILE = join(CONFIG_DIR, "pro-rules.cjs");
|
|
@@ -17,7 +18,10 @@ var PRO_RULES_META = join(CONFIG_DIR, "pro-rules-meta.json");
|
|
|
17
18
|
var API_BASE = process.env.XPLOITSCAN_API_URL ?? "https://api.xploitscan.com";
|
|
18
19
|
var WEB_BASE = process.env.XPLOITSCAN_WEB_URL ?? "https://xploitscan.com";
|
|
19
20
|
if (API_BASE.startsWith("http://") && !API_BASE.includes("localhost") && !API_BASE.includes("127.0.0.1")) {
|
|
20
|
-
|
|
21
|
+
throw new Error("API URL must use HTTPS for security. Set XPLOITSCAN_API_URL to an https:// URL.");
|
|
22
|
+
}
|
|
23
|
+
if (WEB_BASE.startsWith("http://") && !WEB_BASE.includes("localhost") && !WEB_BASE.includes("127.0.0.1")) {
|
|
24
|
+
throw new Error("Web URL must use HTTPS for security. Set XPLOITSCAN_WEB_URL to an https:// URL.");
|
|
21
25
|
}
|
|
22
26
|
function getStoredToken() {
|
|
23
27
|
try {
|
|
@@ -67,7 +71,9 @@ async function checkUsage() {
|
|
|
67
71
|
return { allowed: true, plan: "anonymous", remaining: -1, limit: -1 };
|
|
68
72
|
}
|
|
69
73
|
try {
|
|
70
|
-
const res = await
|
|
74
|
+
const res = await fetch(`${WEB_BASE}/api/cli/usage`, {
|
|
75
|
+
headers: { Authorization: `Bearer ${token.token}` }
|
|
76
|
+
});
|
|
71
77
|
if (!res.ok) {
|
|
72
78
|
return { allowed: true, plan: "unknown", remaining: -1, limit: -1 };
|
|
73
79
|
}
|
|
@@ -96,11 +102,15 @@ async function uploadScanResults(result) {
|
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
async function syncUser() {
|
|
105
|
+
const token = getStoredToken();
|
|
106
|
+
if (!token) return null;
|
|
99
107
|
try {
|
|
100
|
-
const res = await
|
|
108
|
+
const res = await fetch(`${WEB_BASE}/api/cli/usage`, {
|
|
109
|
+
headers: { Authorization: `Bearer ${token.token}` }
|
|
110
|
+
});
|
|
101
111
|
if (!res.ok) return null;
|
|
102
112
|
const data = await res.json();
|
|
103
|
-
return data.
|
|
113
|
+
return { plan: data.plan, email: token.email };
|
|
104
114
|
} catch {
|
|
105
115
|
return null;
|
|
106
116
|
}
|
|
@@ -126,11 +136,16 @@ async function downloadProRulesBundle() {
|
|
|
126
136
|
if (!res.ok) return false;
|
|
127
137
|
const bundle = await res.text();
|
|
128
138
|
if (!bundle || bundle.length < 100) return false;
|
|
139
|
+
if (!bundle.includes("proOnlyRules") || !bundle.includes("exports")) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const hash = createHash("sha256").update(bundle).digest("hex");
|
|
129
143
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
130
144
|
writeFileSync(PRO_RULES_FILE, bundle, { mode: 384 });
|
|
131
145
|
writeFileSync(PRO_RULES_META, JSON.stringify({
|
|
132
146
|
cachedAt: Date.now(),
|
|
133
|
-
expiresAt: Date.now() + PRO_RULES_CACHE_HOURS * 60 * 60 * 1e3
|
|
147
|
+
expiresAt: Date.now() + PRO_RULES_CACHE_HOURS * 60 * 60 * 1e3,
|
|
148
|
+
sha256: hash
|
|
134
149
|
}), { mode: 384 });
|
|
135
150
|
return true;
|
|
136
151
|
} catch {
|
|
@@ -145,6 +160,14 @@ function loadCachedProRules() {
|
|
|
145
160
|
clearProRulesCache();
|
|
146
161
|
return null;
|
|
147
162
|
}
|
|
163
|
+
if (meta.sha256) {
|
|
164
|
+
const fileContent = readFileSync(PRO_RULES_FILE, "utf-8");
|
|
165
|
+
const currentHash = createHash("sha256").update(fileContent).digest("hex");
|
|
166
|
+
if (currentHash !== meta.sha256) {
|
|
167
|
+
clearProRulesCache();
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
148
171
|
const mod = __require(PRO_RULES_FILE);
|
|
149
172
|
return mod.proOnlyRules || null;
|
|
150
173
|
} catch {
|
|
@@ -173,4 +196,4 @@ export {
|
|
|
173
196
|
loadCachedProRules,
|
|
174
197
|
clearProRulesCache
|
|
175
198
|
};
|
|
176
|
-
//# sourceMappingURL=chunk-
|
|
199
|
+
//# sourceMappingURL=chunk-RJUUWD2F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/api.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { createHash } from \"node:crypto\";\n\nconst CONFIG_DIR = join(homedir(), \".xploitscan\");\nconst TOKEN_FILE = join(CONFIG_DIR, \"token.json\");\nconst PRO_RULES_FILE = join(CONFIG_DIR, \"pro-rules.cjs\");\nconst PRO_RULES_META = join(CONFIG_DIR, \"pro-rules-meta.json\");\n\nconst API_BASE = process.env.XPLOITSCAN_API_URL ?? \"https://api.xploitscan.com\";\nconst WEB_BASE = process.env.XPLOITSCAN_WEB_URL ?? \"https://xploitscan.com\";\n\nif (API_BASE.startsWith(\"http://\") && !API_BASE.includes(\"localhost\") && !API_BASE.includes(\"127.0.0.1\")) {\n throw new Error(\"API URL must use HTTPS for security. Set XPLOITSCAN_API_URL to an https:// URL.\");\n}\nif (WEB_BASE.startsWith(\"http://\") && !WEB_BASE.includes(\"localhost\") && !WEB_BASE.includes(\"127.0.0.1\")) {\n throw new Error(\"Web URL must use HTTPS for security. Set XPLOITSCAN_WEB_URL to an https:// URL.\");\n}\n\ninterface TokenData {\n token: string;\n userId: string;\n email: string;\n expiresAt?: number;\n}\n\nexport function getStoredToken(): TokenData | null {\n try {\n if (!existsSync(TOKEN_FILE)) return null;\n const data = JSON.parse(readFileSync(TOKEN_FILE, \"utf-8\"));\n if (data.expiresAt && Date.now() > data.expiresAt) {\n // Token expired\n unlinkSync(TOKEN_FILE);\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\nexport function storeToken(data: TokenData): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\nexport function clearToken(): void {\n try {\n if (existsSync(TOKEN_FILE)) {\n unlinkSync(TOKEN_FILE);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isAuthenticated(): boolean {\n return getStoredToken() !== null;\n}\n\nasync function apiRequest(\n path: string,\n options: RequestInit = {},\n): Promise<Response> {\n const token = getStoredToken();\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string>),\n };\n\n if (token) {\n headers.Authorization = `Bearer ${token.token}`;\n }\n\n return fetch(`${API_BASE}${path}`, {\n ...options,\n headers,\n });\n}\n\nexport async function checkUsage(): Promise<{\n allowed: boolean;\n plan: string;\n remaining: number;\n limit: number;\n}> {\n const token = getStoredToken();\n if (!token) {\n // Unauthenticated users get limited local scans\n return { allowed: true, plan: \"anonymous\", remaining: -1, limit: -1 };\n }\n\n try {\n // Use web app API for real subscription lookup\n const res = await fetch(`${WEB_BASE}/api/cli/usage`, {\n headers: { Authorization: `Bearer ${token.token}` },\n });\n if (!res.ok) {\n return { allowed: true, plan: \"unknown\", remaining: -1, limit: -1 };\n }\n return await res.json();\n } catch {\n // Network error — allow local scan\n return { allowed: true, plan: \"offline\", remaining: -1, limit: -1 };\n }\n}\n\nexport async function incrementUsage(): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/usage/increment\", { method: \"POST\" });\n } catch {\n // Silent fail — don't block scan\n }\n}\n\nexport async function uploadScanResults(result: {\n directory: string;\n filesScanned: number;\n findings: unknown[];\n duration: number;\n}): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/scans\", {\n method: \"POST\",\n body: JSON.stringify(result),\n });\n } catch {\n // Silent fail\n }\n}\n\nexport async function syncUser(): Promise<{ plan: string; email: string } | null> {\n const token = getStoredToken();\n if (!token) return null;\n try {\n const res = await fetch(`${WEB_BASE}/api/cli/usage`, {\n headers: { Authorization: `Bearer ${token.token}` },\n });\n if (!res.ok) return null;\n const data = await res.json();\n return { plan: data.plan, email: token.email };\n } catch {\n return null;\n }\n}\n\nexport async function getCheckoutUrl(): Promise<string | null> {\n try {\n const res = await apiRequest(\"/api/billing/checkout\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.url;\n } catch {\n return null;\n }\n}\n\n// ── Pro Rules Bundle (Server-Side Delivery) ──────────────────────────────\n\nconst PRO_RULES_CACHE_HOURS = 24;\n\nexport async function downloadProRulesBundle(): Promise<boolean> {\n const token = getStoredToken();\n if (!token) return false;\n\n try {\n const res = await fetch(`${WEB_BASE}/api/cli/rules-bundle`, {\n headers: { Authorization: `Bearer ${token.token}` },\n });\n if (!res.ok) return false;\n\n const bundle = await res.text();\n if (!bundle || bundle.length < 100) return false;\n\n // Basic integrity check: verify bundle contains expected exports\n if (!bundle.includes(\"proOnlyRules\") || !bundle.includes(\"exports\")) {\n return false;\n }\n\n const hash = createHash(\"sha256\").update(bundle).digest(\"hex\");\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(PRO_RULES_FILE, bundle, { mode: 0o600 });\n writeFileSync(PRO_RULES_META, JSON.stringify({\n cachedAt: Date.now(),\n expiresAt: Date.now() + PRO_RULES_CACHE_HOURS * 60 * 60 * 1000,\n sha256: hash,\n }), { mode: 0o600 });\n return true;\n } catch {\n return false;\n }\n}\n\nexport function loadCachedProRules(): unknown[] | null {\n try {\n if (!existsSync(PRO_RULES_FILE) || !existsSync(PRO_RULES_META)) return null;\n\n const meta = JSON.parse(readFileSync(PRO_RULES_META, \"utf-8\"));\n if (Date.now() > meta.expiresAt) {\n clearProRulesCache();\n return null;\n }\n\n // Verify integrity before loading\n if (meta.sha256) {\n const fileContent = readFileSync(PRO_RULES_FILE, \"utf-8\");\n const currentHash = createHash(\"sha256\").update(fileContent).digest(\"hex\");\n if (currentHash !== meta.sha256) {\n clearProRulesCache();\n return null;\n }\n }\n\n // Dynamic require of the cached CJS bundle\n const mod = require(PRO_RULES_FILE);\n return mod.proOnlyRules || null;\n } catch {\n return null;\n }\n}\n\nexport function clearProRulesCache(): void {\n try {\n if (existsSync(PRO_RULES_FILE)) unlinkSync(PRO_RULES_FILE);\n if (existsSync(PRO_RULES_META)) unlinkSync(PRO_RULES_META);\n } catch { /* ignore */ }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
storeToken,
|
|
12
12
|
syncUser,
|
|
13
13
|
uploadScanResults
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-RJUUWD2F.js";
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
17
17
|
import { Command } from "commander";
|
|
@@ -1806,13 +1806,15 @@ function chunkFiles(files, maxChars) {
|
|
|
1806
1806
|
let current = [];
|
|
1807
1807
|
let currentSize = 0;
|
|
1808
1808
|
for (const file of files) {
|
|
1809
|
-
|
|
1809
|
+
const truncatedContent = file.content.length > maxChars ? file.content.slice(0, maxChars) + "\n// ... truncated for analysis" : file.content;
|
|
1810
|
+
const entry = { path: file.path, content: truncatedContent };
|
|
1811
|
+
if (currentSize + entry.content.length > maxChars && current.length > 0) {
|
|
1810
1812
|
chunks.push(current);
|
|
1811
1813
|
current = [];
|
|
1812
1814
|
currentSize = 0;
|
|
1813
1815
|
}
|
|
1814
|
-
current.push(
|
|
1815
|
-
currentSize +=
|
|
1816
|
+
current.push(entry);
|
|
1817
|
+
currentSize += entry.content.length;
|
|
1816
1818
|
}
|
|
1817
1819
|
if (current.length > 0) chunks.push(current);
|
|
1818
1820
|
return chunks;
|
|
@@ -3093,8 +3095,8 @@ async function scanCommand(directory, options) {
|
|
|
3093
3095
|
if (options.diff) {
|
|
3094
3096
|
const base = typeof options.diff === "string" ? options.diff : "main";
|
|
3095
3097
|
try {
|
|
3096
|
-
const { execSync } = await import("child_process");
|
|
3097
|
-
const changedFiles =
|
|
3098
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
3099
|
+
const changedFiles = execSync2(`git diff --name-only ${base}`, { cwd: dir, encoding: "utf-8" }).trim().split("\n").filter(Boolean);
|
|
3098
3100
|
files = files.filter((f) => changedFiles.some((cf) => f.endsWith(cf) || cf.endsWith(f)));
|
|
3099
3101
|
if (!isSilent) {
|
|
3100
3102
|
spinner.info(chalk2.gray(` Diff mode: scanning ${files.length} changed files vs ${base}`));
|
|
@@ -3414,32 +3416,8 @@ async function waitForBrowserLogin() {
|
|
|
3414
3416
|
}
|
|
3415
3417
|
} else if (url.pathname === "/login") {
|
|
3416
3418
|
const callbackUrl = `http://localhost:${server.address().port}/callback`;
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
res.end(`
|
|
3420
|
-
<html>
|
|
3421
|
-
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;">
|
|
3422
|
-
<div style="text-align: center;">
|
|
3423
|
-
<h1 style="color: #00d4ff;">xploitscan</h1>
|
|
3424
|
-
<p>Redirecting to login (dev mode)...</p>
|
|
3425
|
-
<script>
|
|
3426
|
-
const params = new URLSearchParams({
|
|
3427
|
-
token: 'dev_token_' + Date.now(),
|
|
3428
|
-
email: 'dev@xploitscan.com',
|
|
3429
|
-
user_id: 'user_dev_' + Date.now(),
|
|
3430
|
-
state: '${expectedState}'
|
|
3431
|
-
});
|
|
3432
|
-
setTimeout(() => {
|
|
3433
|
-
window.location.href = '/callback?' + params.toString();
|
|
3434
|
-
}, 1000);
|
|
3435
|
-
</script>
|
|
3436
|
-
</div>
|
|
3437
|
-
</body>
|
|
3438
|
-
</html>
|
|
3439
|
-
`);
|
|
3440
|
-
} else {
|
|
3441
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
3442
|
-
res.end(`
|
|
3419
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
3420
|
+
res.end(`
|
|
3443
3421
|
<html>
|
|
3444
3422
|
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;">
|
|
3445
3423
|
<div style="text-align: center;">
|
|
@@ -3450,7 +3428,6 @@ async function waitForBrowserLogin() {
|
|
|
3450
3428
|
</body>
|
|
3451
3429
|
</html>
|
|
3452
3430
|
`);
|
|
3453
|
-
}
|
|
3454
3431
|
} else {
|
|
3455
3432
|
res.writeHead(302, { Location: "/login" });
|
|
3456
3433
|
res.end();
|
|
@@ -3474,11 +3451,152 @@ Open this URL in your browser to log in:`));
|
|
|
3474
3451
|
});
|
|
3475
3452
|
}
|
|
3476
3453
|
|
|
3454
|
+
// src/commands/hook.ts
|
|
3455
|
+
import { execSync } from "child_process";
|
|
3456
|
+
import {
|
|
3457
|
+
existsSync as existsSync4,
|
|
3458
|
+
readFileSync as readFileSync3,
|
|
3459
|
+
writeFileSync,
|
|
3460
|
+
chmodSync,
|
|
3461
|
+
unlinkSync
|
|
3462
|
+
} from "fs";
|
|
3463
|
+
import { join as join6 } from "path";
|
|
3464
|
+
import chalk4 from "chalk";
|
|
3465
|
+
var HOOK_START_MARKER = "# xploitscan-hook-start";
|
|
3466
|
+
var HOOK_END_MARKER = "# xploitscan-hook-end";
|
|
3467
|
+
var HOOK_CONTENT = `${HOOK_START_MARKER}
|
|
3468
|
+
# Installed by XploitScan CLI \u2014 do not edit this block manually
|
|
3469
|
+
echo ""
|
|
3470
|
+
echo "\u{1F6E1} Running XploitScan security check..."
|
|
3471
|
+
npx --yes xploitscan scan . --no-ai --diff HEAD
|
|
3472
|
+
HOOK_EXIT=$?
|
|
3473
|
+
if [ $HOOK_EXIT -ne 0 ]; then
|
|
3474
|
+
echo ""
|
|
3475
|
+
echo "\u274C XploitScan found security issues. Commit blocked."
|
|
3476
|
+
echo " Fix the issues above, or use 'git commit --no-verify' to skip."
|
|
3477
|
+
exit 1
|
|
3478
|
+
fi
|
|
3479
|
+
${HOOK_END_MARKER}`;
|
|
3480
|
+
function getGitRoot() {
|
|
3481
|
+
try {
|
|
3482
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
3483
|
+
encoding: "utf-8",
|
|
3484
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
3485
|
+
}).trim();
|
|
3486
|
+
return root || null;
|
|
3487
|
+
} catch {
|
|
3488
|
+
return null;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
function removeExistingBlock(content) {
|
|
3492
|
+
const startIdx = content.indexOf(HOOK_START_MARKER);
|
|
3493
|
+
const endIdx = content.indexOf(HOOK_END_MARKER);
|
|
3494
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
3495
|
+
return content;
|
|
3496
|
+
}
|
|
3497
|
+
const before = content.slice(0, startIdx).replace(/\n+$/, "");
|
|
3498
|
+
const after = content.slice(endIdx + HOOK_END_MARKER.length).replace(/^\n+/, "");
|
|
3499
|
+
if (before && after) return `${before}
|
|
3500
|
+
|
|
3501
|
+
${after}
|
|
3502
|
+
`;
|
|
3503
|
+
if (before) return `${before}
|
|
3504
|
+
`;
|
|
3505
|
+
if (after) return `${after}
|
|
3506
|
+
`;
|
|
3507
|
+
return "";
|
|
3508
|
+
}
|
|
3509
|
+
async function installHookCommand(options) {
|
|
3510
|
+
const gitRoot = getGitRoot();
|
|
3511
|
+
if (!gitRoot) {
|
|
3512
|
+
console.log(chalk4.red("\u2717 Not a git repository."));
|
|
3513
|
+
console.log(chalk4.gray(" Run this command from inside a git repo."));
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
}
|
|
3516
|
+
const hooksDir = join6(gitRoot, ".git", "hooks");
|
|
3517
|
+
const hookPath = join6(hooksDir, "pre-commit");
|
|
3518
|
+
let existingContent = "";
|
|
3519
|
+
let hadExisting = false;
|
|
3520
|
+
if (existsSync4(hookPath)) {
|
|
3521
|
+
existingContent = readFileSync3(hookPath, "utf-8");
|
|
3522
|
+
hadExisting = true;
|
|
3523
|
+
const alreadyInstalled = existingContent.includes(HOOK_START_MARKER) && existingContent.includes(HOOK_END_MARKER);
|
|
3524
|
+
if (alreadyInstalled && !options.force) {
|
|
3525
|
+
console.log(chalk4.yellow("\u26A0 XploitScan hook is already installed."));
|
|
3526
|
+
console.log(chalk4.gray(" Use --force to reinstall, or `xploitscan hook uninstall` to remove it."));
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
if (alreadyInstalled && options.force) {
|
|
3530
|
+
existingContent = removeExistingBlock(existingContent);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
let newContent;
|
|
3534
|
+
if (hadExisting && existingContent.trim()) {
|
|
3535
|
+
const trimmed = existingContent.replace(/\n+$/, "");
|
|
3536
|
+
newContent = `${trimmed}
|
|
3537
|
+
|
|
3538
|
+
${HOOK_CONTENT}
|
|
3539
|
+
`;
|
|
3540
|
+
} else {
|
|
3541
|
+
newContent = `#!/bin/sh
|
|
3542
|
+
|
|
3543
|
+
${HOOK_CONTENT}
|
|
3544
|
+
`;
|
|
3545
|
+
}
|
|
3546
|
+
try {
|
|
3547
|
+
writeFileSync(hookPath, newContent, "utf-8");
|
|
3548
|
+
chmodSync(hookPath, 493);
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
console.log(chalk4.red(`\u2717 Failed to write pre-commit hook: ${err.message}`));
|
|
3551
|
+
process.exit(1);
|
|
3552
|
+
}
|
|
3553
|
+
console.log(chalk4.green("\u2713 XploitScan pre-commit hook installed."));
|
|
3554
|
+
console.log("");
|
|
3555
|
+
console.log(chalk4.gray(" XploitScan will now scan your code before every commit."));
|
|
3556
|
+
console.log(chalk4.gray(" To skip a scan, use: ") + chalk4.cyan("git commit --no-verify"));
|
|
3557
|
+
console.log(chalk4.gray(" To remove: ") + chalk4.cyan("xploitscan hook uninstall"));
|
|
3558
|
+
}
|
|
3559
|
+
async function uninstallHookCommand() {
|
|
3560
|
+
const gitRoot = getGitRoot();
|
|
3561
|
+
if (!gitRoot) {
|
|
3562
|
+
console.log(chalk4.red("\u2717 Not a git repository."));
|
|
3563
|
+
console.log(chalk4.gray(" Run this command from inside a git repo."));
|
|
3564
|
+
process.exit(1);
|
|
3565
|
+
}
|
|
3566
|
+
const hookPath = join6(gitRoot, ".git", "hooks", "pre-commit");
|
|
3567
|
+
if (!existsSync4(hookPath)) {
|
|
3568
|
+
console.log(chalk4.yellow("\u26A0 No pre-commit hook found. Nothing to uninstall."));
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
const content = readFileSync3(hookPath, "utf-8");
|
|
3572
|
+
if (!content.includes(HOOK_START_MARKER)) {
|
|
3573
|
+
console.log(chalk4.yellow("\u26A0 XploitScan hook not found in pre-commit file."));
|
|
3574
|
+
console.log(chalk4.gray(" Nothing to remove."));
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
const stripped = removeExistingBlock(content).trim();
|
|
3578
|
+
const shebangOnly = stripped === "#!/bin/sh" || stripped === "#!/usr/bin/env sh" || stripped === "";
|
|
3579
|
+
try {
|
|
3580
|
+
if (shebangOnly) {
|
|
3581
|
+
unlinkSync(hookPath);
|
|
3582
|
+
console.log(chalk4.green("\u2713 XploitScan hook removed. Pre-commit file deleted (was empty after removal)."));
|
|
3583
|
+
} else {
|
|
3584
|
+
writeFileSync(hookPath, stripped + "\n", "utf-8");
|
|
3585
|
+
chmodSync(hookPath, 493);
|
|
3586
|
+
console.log(chalk4.green("\u2713 XploitScan hook removed."));
|
|
3587
|
+
console.log(chalk4.gray(" Other hook content was preserved."));
|
|
3588
|
+
}
|
|
3589
|
+
} catch (err) {
|
|
3590
|
+
console.log(chalk4.red(`\u2717 Failed to uninstall hook: ${err.message}`));
|
|
3591
|
+
process.exit(1);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3477
3595
|
// src/index.ts
|
|
3478
3596
|
var program = new Command();
|
|
3479
3597
|
program.name("xploitscan").description(
|
|
3480
3598
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
3481
|
-
).version("0.
|
|
3599
|
+
).version("1.0.3");
|
|
3482
3600
|
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) => {
|
|
3483
3601
|
await scanCommand(directory, {
|
|
3484
3602
|
directory,
|
|
@@ -3493,26 +3611,31 @@ var auth = program.command("auth").description("Manage authentication");
|
|
|
3493
3611
|
auth.command("login").description("Log in to your XploitScan account").action(loginCommand);
|
|
3494
3612
|
auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
|
|
3495
3613
|
auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
|
|
3614
|
+
var hook = program.command("hook").description("Manage git pre-commit hooks for automatic scanning");
|
|
3615
|
+
hook.command("install").description("Install a git pre-commit hook that runs XploitScan on every commit").option("-f, --force", "Overwrite existing XploitScan hook without prompting", false).action(async (opts) => {
|
|
3616
|
+
await installHookCommand({ force: opts.force });
|
|
3617
|
+
});
|
|
3618
|
+
hook.command("uninstall").description("Remove the XploitScan pre-commit hook").action(uninstallHookCommand);
|
|
3496
3619
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
3497
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
3498
|
-
const
|
|
3620
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-HTHCG6QE.js");
|
|
3621
|
+
const chalk5 = (await import("chalk")).default;
|
|
3499
3622
|
const token = getStoredToken2();
|
|
3500
3623
|
if (!token) {
|
|
3501
|
-
console.log(
|
|
3624
|
+
console.log(chalk5.yellow("Please log in first: xploitscan auth login"));
|
|
3502
3625
|
return;
|
|
3503
3626
|
}
|
|
3504
|
-
console.log(
|
|
3627
|
+
console.log(chalk5.cyan("Creating checkout session..."));
|
|
3505
3628
|
const url = await getCheckoutUrl();
|
|
3506
3629
|
if (url) {
|
|
3507
|
-
console.log(
|
|
3630
|
+
console.log(chalk5.green(`
|
|
3508
3631
|
Open this URL to upgrade:`));
|
|
3509
|
-
console.log(
|
|
3632
|
+
console.log(chalk5.bold.underline(url));
|
|
3510
3633
|
const { execFile: execFile4 } = await import("child_process");
|
|
3511
3634
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3512
3635
|
execFile4(openCmd, [url], () => {
|
|
3513
3636
|
});
|
|
3514
3637
|
} else {
|
|
3515
|
-
console.log(
|
|
3638
|
+
console.log(chalk5.red("Failed to create checkout session. Please try again."));
|
|
3516
3639
|
}
|
|
3517
3640
|
});
|
|
3518
3641
|
program.parse();
|