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.
@@ -12,7 +12,7 @@ import {
12
12
  storeToken,
13
13
  syncUser,
14
14
  uploadScanResults
15
- } from "./chunk-IHRV7UHG.js";
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-ZNWEMMEL.js.map
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
- console.warn("WARNING: API URL is not using HTTPS. This is insecure.");
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 apiRequest("/api/usage/check");
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 apiRequest("/api/users/sync", { method: "POST" });
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.user;
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-IHRV7UHG.js.map
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-IHRV7UHG.js";
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
- if (currentSize + file.content.length > maxChars && current.length > 0) {
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(file);
1815
- currentSize += file.content.length;
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 = execSync(`git diff --name-only ${base}`, { cwd: dir, encoding: "utf-8" }).trim().split("\n").filter(Boolean);
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
- if (process.env.NODE_ENV === "development" && process.env.XPLOITSCAN_DEV_AUTH === "true") {
3418
- res.writeHead(200, { "Content-Type": "text/html" });
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.8.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-ZNWEMMEL.js");
3498
- const chalk4 = (await import("chalk")).default;
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(chalk4.yellow("Please log in first: xploitscan auth login"));
3624
+ console.log(chalk5.yellow("Please log in first: xploitscan auth login"));
3502
3625
  return;
3503
3626
  }
3504
- console.log(chalk4.cyan("Creating checkout session..."));
3627
+ console.log(chalk5.cyan("Creating checkout session..."));
3505
3628
  const url = await getCheckoutUrl();
3506
3629
  if (url) {
3507
- console.log(chalk4.green(`
3630
+ console.log(chalk5.green(`
3508
3631
  Open this URL to upgrade:`));
3509
- console.log(chalk4.bold.underline(url));
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(chalk4.red("Failed to create checkout session. Please try again."));
3638
+ console.log(chalk5.red("Failed to create checkout session. Please try again."));
3516
3639
  }
3517
3640
  });
3518
3641
  program.parse();