xploitscan 1.0.2 → 1.0.4
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 +168 -49
- 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;
|
|
@@ -3001,13 +3003,13 @@ function renderSarifReport(result) {
|
|
|
3001
3003
|
}
|
|
3002
3004
|
},
|
|
3003
3005
|
results: result.findings.map((f) => {
|
|
3004
|
-
const
|
|
3006
|
+
const messageText = f.fix ? `${f.title}: ${f.description}
|
|
3007
|
+
Suggested fix: ${f.fix}` : `${f.title}: ${f.description}`;
|
|
3008
|
+
return {
|
|
3005
3009
|
ruleId: f.rule,
|
|
3006
3010
|
ruleIndex: ruleIndex.get(f.rule) ?? 0,
|
|
3007
3011
|
level: SEVERITY_TO_SARIF[f.severity],
|
|
3008
|
-
message: {
|
|
3009
|
-
text: `${f.title}: ${f.description}`
|
|
3010
|
-
},
|
|
3012
|
+
message: { text: messageText },
|
|
3011
3013
|
locations: [
|
|
3012
3014
|
{
|
|
3013
3015
|
physicalLocation: {
|
|
@@ -3020,10 +3022,6 @@ function renderSarifReport(result) {
|
|
|
3020
3022
|
}
|
|
3021
3023
|
]
|
|
3022
3024
|
};
|
|
3023
|
-
if (f.fix) {
|
|
3024
|
-
entry.fixes = [{ description: { text: f.fix } }];
|
|
3025
|
-
}
|
|
3026
|
-
return entry;
|
|
3027
3025
|
})
|
|
3028
3026
|
}
|
|
3029
3027
|
]
|
|
@@ -3093,8 +3091,8 @@ async function scanCommand(directory, options) {
|
|
|
3093
3091
|
if (options.diff) {
|
|
3094
3092
|
const base = typeof options.diff === "string" ? options.diff : "main";
|
|
3095
3093
|
try {
|
|
3096
|
-
const { execSync } = await import("child_process");
|
|
3097
|
-
const changedFiles =
|
|
3094
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
3095
|
+
const changedFiles = execSync2(`git diff --name-only ${base}`, { cwd: dir, encoding: "utf-8" }).trim().split("\n").filter(Boolean);
|
|
3098
3096
|
files = files.filter((f) => changedFiles.some((cf) => f.endsWith(cf) || cf.endsWith(f)));
|
|
3099
3097
|
if (!isSilent) {
|
|
3100
3098
|
spinner.info(chalk2.gray(` Diff mode: scanning ${files.length} changed files vs ${base}`));
|
|
@@ -3414,32 +3412,8 @@ async function waitForBrowserLogin() {
|
|
|
3414
3412
|
}
|
|
3415
3413
|
} else if (url.pathname === "/login") {
|
|
3416
3414
|
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(`
|
|
3415
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
3416
|
+
res.end(`
|
|
3443
3417
|
<html>
|
|
3444
3418
|
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0f; color: #e0e0e0;">
|
|
3445
3419
|
<div style="text-align: center;">
|
|
@@ -3450,7 +3424,6 @@ async function waitForBrowserLogin() {
|
|
|
3450
3424
|
</body>
|
|
3451
3425
|
</html>
|
|
3452
3426
|
`);
|
|
3453
|
-
}
|
|
3454
3427
|
} else {
|
|
3455
3428
|
res.writeHead(302, { Location: "/login" });
|
|
3456
3429
|
res.end();
|
|
@@ -3474,11 +3447,152 @@ Open this URL in your browser to log in:`));
|
|
|
3474
3447
|
});
|
|
3475
3448
|
}
|
|
3476
3449
|
|
|
3450
|
+
// src/commands/hook.ts
|
|
3451
|
+
import { execSync } from "child_process";
|
|
3452
|
+
import {
|
|
3453
|
+
existsSync as existsSync4,
|
|
3454
|
+
readFileSync as readFileSync3,
|
|
3455
|
+
writeFileSync,
|
|
3456
|
+
chmodSync,
|
|
3457
|
+
unlinkSync
|
|
3458
|
+
} from "fs";
|
|
3459
|
+
import { join as join6 } from "path";
|
|
3460
|
+
import chalk4 from "chalk";
|
|
3461
|
+
var HOOK_START_MARKER = "# xploitscan-hook-start";
|
|
3462
|
+
var HOOK_END_MARKER = "# xploitscan-hook-end";
|
|
3463
|
+
var HOOK_CONTENT = `${HOOK_START_MARKER}
|
|
3464
|
+
# Installed by XploitScan CLI \u2014 do not edit this block manually
|
|
3465
|
+
echo ""
|
|
3466
|
+
echo "\u{1F6E1} Running XploitScan security check..."
|
|
3467
|
+
npx --yes xploitscan scan . --no-ai --diff HEAD
|
|
3468
|
+
HOOK_EXIT=$?
|
|
3469
|
+
if [ $HOOK_EXIT -ne 0 ]; then
|
|
3470
|
+
echo ""
|
|
3471
|
+
echo "\u274C XploitScan found security issues. Commit blocked."
|
|
3472
|
+
echo " Fix the issues above, or use 'git commit --no-verify' to skip."
|
|
3473
|
+
exit 1
|
|
3474
|
+
fi
|
|
3475
|
+
${HOOK_END_MARKER}`;
|
|
3476
|
+
function getGitRoot() {
|
|
3477
|
+
try {
|
|
3478
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
3479
|
+
encoding: "utf-8",
|
|
3480
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
3481
|
+
}).trim();
|
|
3482
|
+
return root || null;
|
|
3483
|
+
} catch {
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
function removeExistingBlock(content) {
|
|
3488
|
+
const startIdx = content.indexOf(HOOK_START_MARKER);
|
|
3489
|
+
const endIdx = content.indexOf(HOOK_END_MARKER);
|
|
3490
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
3491
|
+
return content;
|
|
3492
|
+
}
|
|
3493
|
+
const before = content.slice(0, startIdx).replace(/\n+$/, "");
|
|
3494
|
+
const after = content.slice(endIdx + HOOK_END_MARKER.length).replace(/^\n+/, "");
|
|
3495
|
+
if (before && after) return `${before}
|
|
3496
|
+
|
|
3497
|
+
${after}
|
|
3498
|
+
`;
|
|
3499
|
+
if (before) return `${before}
|
|
3500
|
+
`;
|
|
3501
|
+
if (after) return `${after}
|
|
3502
|
+
`;
|
|
3503
|
+
return "";
|
|
3504
|
+
}
|
|
3505
|
+
async function installHookCommand(options) {
|
|
3506
|
+
const gitRoot = getGitRoot();
|
|
3507
|
+
if (!gitRoot) {
|
|
3508
|
+
console.log(chalk4.red("\u2717 Not a git repository."));
|
|
3509
|
+
console.log(chalk4.gray(" Run this command from inside a git repo."));
|
|
3510
|
+
process.exit(1);
|
|
3511
|
+
}
|
|
3512
|
+
const hooksDir = join6(gitRoot, ".git", "hooks");
|
|
3513
|
+
const hookPath = join6(hooksDir, "pre-commit");
|
|
3514
|
+
let existingContent = "";
|
|
3515
|
+
let hadExisting = false;
|
|
3516
|
+
if (existsSync4(hookPath)) {
|
|
3517
|
+
existingContent = readFileSync3(hookPath, "utf-8");
|
|
3518
|
+
hadExisting = true;
|
|
3519
|
+
const alreadyInstalled = existingContent.includes(HOOK_START_MARKER) && existingContent.includes(HOOK_END_MARKER);
|
|
3520
|
+
if (alreadyInstalled && !options.force) {
|
|
3521
|
+
console.log(chalk4.yellow("\u26A0 XploitScan hook is already installed."));
|
|
3522
|
+
console.log(chalk4.gray(" Use --force to reinstall, or `xploitscan hook uninstall` to remove it."));
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
if (alreadyInstalled && options.force) {
|
|
3526
|
+
existingContent = removeExistingBlock(existingContent);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
let newContent;
|
|
3530
|
+
if (hadExisting && existingContent.trim()) {
|
|
3531
|
+
const trimmed = existingContent.replace(/\n+$/, "");
|
|
3532
|
+
newContent = `${trimmed}
|
|
3533
|
+
|
|
3534
|
+
${HOOK_CONTENT}
|
|
3535
|
+
`;
|
|
3536
|
+
} else {
|
|
3537
|
+
newContent = `#!/bin/sh
|
|
3538
|
+
|
|
3539
|
+
${HOOK_CONTENT}
|
|
3540
|
+
`;
|
|
3541
|
+
}
|
|
3542
|
+
try {
|
|
3543
|
+
writeFileSync(hookPath, newContent, "utf-8");
|
|
3544
|
+
chmodSync(hookPath, 493);
|
|
3545
|
+
} catch (err) {
|
|
3546
|
+
console.log(chalk4.red(`\u2717 Failed to write pre-commit hook: ${err.message}`));
|
|
3547
|
+
process.exit(1);
|
|
3548
|
+
}
|
|
3549
|
+
console.log(chalk4.green("\u2713 XploitScan pre-commit hook installed."));
|
|
3550
|
+
console.log("");
|
|
3551
|
+
console.log(chalk4.gray(" XploitScan will now scan your code before every commit."));
|
|
3552
|
+
console.log(chalk4.gray(" To skip a scan, use: ") + chalk4.cyan("git commit --no-verify"));
|
|
3553
|
+
console.log(chalk4.gray(" To remove: ") + chalk4.cyan("xploitscan hook uninstall"));
|
|
3554
|
+
}
|
|
3555
|
+
async function uninstallHookCommand() {
|
|
3556
|
+
const gitRoot = getGitRoot();
|
|
3557
|
+
if (!gitRoot) {
|
|
3558
|
+
console.log(chalk4.red("\u2717 Not a git repository."));
|
|
3559
|
+
console.log(chalk4.gray(" Run this command from inside a git repo."));
|
|
3560
|
+
process.exit(1);
|
|
3561
|
+
}
|
|
3562
|
+
const hookPath = join6(gitRoot, ".git", "hooks", "pre-commit");
|
|
3563
|
+
if (!existsSync4(hookPath)) {
|
|
3564
|
+
console.log(chalk4.yellow("\u26A0 No pre-commit hook found. Nothing to uninstall."));
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
const content = readFileSync3(hookPath, "utf-8");
|
|
3568
|
+
if (!content.includes(HOOK_START_MARKER)) {
|
|
3569
|
+
console.log(chalk4.yellow("\u26A0 XploitScan hook not found in pre-commit file."));
|
|
3570
|
+
console.log(chalk4.gray(" Nothing to remove."));
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
const stripped = removeExistingBlock(content).trim();
|
|
3574
|
+
const shebangOnly = stripped === "#!/bin/sh" || stripped === "#!/usr/bin/env sh" || stripped === "";
|
|
3575
|
+
try {
|
|
3576
|
+
if (shebangOnly) {
|
|
3577
|
+
unlinkSync(hookPath);
|
|
3578
|
+
console.log(chalk4.green("\u2713 XploitScan hook removed. Pre-commit file deleted (was empty after removal)."));
|
|
3579
|
+
} else {
|
|
3580
|
+
writeFileSync(hookPath, stripped + "\n", "utf-8");
|
|
3581
|
+
chmodSync(hookPath, 493);
|
|
3582
|
+
console.log(chalk4.green("\u2713 XploitScan hook removed."));
|
|
3583
|
+
console.log(chalk4.gray(" Other hook content was preserved."));
|
|
3584
|
+
}
|
|
3585
|
+
} catch (err) {
|
|
3586
|
+
console.log(chalk4.red(`\u2717 Failed to uninstall hook: ${err.message}`));
|
|
3587
|
+
process.exit(1);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3477
3591
|
// src/index.ts
|
|
3478
3592
|
var program = new Command();
|
|
3479
3593
|
program.name("xploitscan").description(
|
|
3480
3594
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
3481
|
-
).version("0.
|
|
3595
|
+
).version("1.0.4");
|
|
3482
3596
|
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
3597
|
await scanCommand(directory, {
|
|
3484
3598
|
directory,
|
|
@@ -3493,26 +3607,31 @@ var auth = program.command("auth").description("Manage authentication");
|
|
|
3493
3607
|
auth.command("login").description("Log in to your XploitScan account").action(loginCommand);
|
|
3494
3608
|
auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
|
|
3495
3609
|
auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
|
|
3610
|
+
var hook = program.command("hook").description("Manage git pre-commit hooks for automatic scanning");
|
|
3611
|
+
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) => {
|
|
3612
|
+
await installHookCommand({ force: opts.force });
|
|
3613
|
+
});
|
|
3614
|
+
hook.command("uninstall").description("Remove the XploitScan pre-commit hook").action(uninstallHookCommand);
|
|
3496
3615
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
3497
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
3498
|
-
const
|
|
3616
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-HTHCG6QE.js");
|
|
3617
|
+
const chalk5 = (await import("chalk")).default;
|
|
3499
3618
|
const token = getStoredToken2();
|
|
3500
3619
|
if (!token) {
|
|
3501
|
-
console.log(
|
|
3620
|
+
console.log(chalk5.yellow("Please log in first: xploitscan auth login"));
|
|
3502
3621
|
return;
|
|
3503
3622
|
}
|
|
3504
|
-
console.log(
|
|
3623
|
+
console.log(chalk5.cyan("Creating checkout session..."));
|
|
3505
3624
|
const url = await getCheckoutUrl();
|
|
3506
3625
|
if (url) {
|
|
3507
|
-
console.log(
|
|
3626
|
+
console.log(chalk5.green(`
|
|
3508
3627
|
Open this URL to upgrade:`));
|
|
3509
|
-
console.log(
|
|
3628
|
+
console.log(chalk5.bold.underline(url));
|
|
3510
3629
|
const { execFile: execFile4 } = await import("child_process");
|
|
3511
3630
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3512
3631
|
execFile4(openCmd, [url], () => {
|
|
3513
3632
|
});
|
|
3514
3633
|
} else {
|
|
3515
|
-
console.log(
|
|
3634
|
+
console.log(chalk5.red("Failed to create checkout session. Please try again."));
|
|
3516
3635
|
}
|
|
3517
3636
|
});
|
|
3518
3637
|
program.parse();
|