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.
@@ -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":";;;;;;;;;AAAA,SAAS,cAAc,eAAe,WAAW,YAAY,kBAAkB;AAC/E,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAE3B,IAAM,aAAa,KAAK,QAAQ,GAAG,aAAa;AAChD,IAAM,aAAa,KAAK,YAAY,YAAY;AAChD,IAAM,iBAAiB,KAAK,YAAY,eAAe;AACvD,IAAM,iBAAiB,KAAK,YAAY,qBAAqB;AAE7D,IAAM,WAAW,QAAQ,IAAI,sBAAsB;AACnD,IAAM,WAAW,QAAQ,IAAI,sBAAsB;AAEnD,IAAI,SAAS,WAAW,SAAS,KAAK,CAAC,SAAS,SAAS,WAAW,KAAK,CAAC,SAAS,SAAS,WAAW,GAAG;AACxG,QAAM,IAAI,MAAM,iFAAiF;AACnG;AACA,IAAI,SAAS,WAAW,SAAS,KAAK,CAAC,SAAS,SAAS,WAAW,KAAK,CAAC,SAAS,SAAS,WAAW,GAAG;AACxG,QAAM,IAAI,MAAM,iFAAiF;AACnG;AASO,SAAS,iBAAmC;AACjD,MAAI;AACF,QAAI,CAAC,WAAW,UAAU,EAAG,QAAO;AACpC,UAAM,OAAO,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AACzD,QAAI,KAAK,aAAa,KAAK,IAAI,IAAI,KAAK,WAAW;AAEjD,iBAAW,UAAU;AACrB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,WAAW,MAAuB;AAChD,YAAU,YAAY,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACtD,gBAAc,YAAY,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;AAC1E;AAEO,SAAS,aAAmB;AACjC,MAAI;AACF,QAAI,WAAW,UAAU,GAAG;AAC1B,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,kBAA2B;AACzC,SAAO,eAAe,MAAM;AAC9B;AAEA,eAAe,WACb,MACA,UAAuB,CAAC,GACL;AACnB,QAAM,QAAQ,eAAe;AAC7B,QAAM,UAAkC;AAAA,IACtC,gBAAgB;AAAA,IAChB,GAAI,QAAQ;AAAA,EACd;AAEA,MAAI,OAAO;AACT,YAAQ,gBAAgB,UAAU,MAAM,KAAK;AAAA,EAC/C;AAEA,SAAO,MAAM,GAAG,QAAQ,GAAG,IAAI,IAAI;AAAA,IACjC,GAAG;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,aAKnB;AACD,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,OAAO;AAEV,WAAO,EAAE,SAAS,MAAM,MAAM,aAAa,WAAW,IAAI,OAAO,GAAG;AAAA,EACtE;AAEA,MAAI;AAEF,UAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,kBAAkB;AAAA,MACnD,SAAS,EAAE,eAAe,UAAU,MAAM,KAAK,GAAG;AAAA,IACpD,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,aAAO,EAAE,SAAS,MAAM,MAAM,WAAW,WAAW,IAAI,OAAO,GAAG;AAAA,IACpE;AACA,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AAEN,WAAO,EAAE,SAAS,MAAM,MAAM,WAAW,WAAW,IAAI,OAAO,GAAG;AAAA,EACpE;AACF;AAEA,eAAsB,iBAAgC;AACpD,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,MAAO;AAEZ,MAAI;AACF,UAAM,WAAW,wBAAwB,EAAE,QAAQ,OAAO,CAAC;AAAA,EAC7D,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,kBAAkB,QAKtB;AAChB,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,MAAO;AAEZ,MAAI;AACF,UAAM,WAAW,cAAc;AAAA,MAC7B,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,MAAM;AAAA,IAC7B,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,WAA4D;AAChF,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,kBAAkB;AAAA,MACnD,SAAS,EAAE,eAAe,UAAU,MAAM,KAAK,GAAG;AAAA,IACpD,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,EAAE,MAAM,KAAK,MAAM,OAAO,MAAM,MAAM;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAyC;AAC7D,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,yBAAyB,EAAE,QAAQ,OAAO,CAAC;AACxE,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,KAAK;AAAA,EACd,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,IAAM,wBAAwB;AAE9B,eAAsB,yBAA2C;AAC/D,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,yBAAyB;AAAA,MAC1D,SAAS,EAAE,eAAe,UAAU,MAAM,KAAK,GAAG;AAAA,IACpD,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AAEpB,UAAM,SAAS,MAAM,IAAI,KAAK;AAC9B,QAAI,CAAC,UAAU,OAAO,SAAS,IAAK,QAAO;AAG3C,QAAI,CAAC,OAAO,SAAS,cAAc,KAAK,CAAC,OAAO,SAAS,SAAS,GAAG;AACnE,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAC7D,cAAU,YAAY,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACtD,kBAAc,gBAAgB,QAAQ,EAAE,MAAM,IAAM,CAAC;AACrD,kBAAc,gBAAgB,KAAK,UAAU;AAAA,MAC3C,UAAU,KAAK,IAAI;AAAA,MACnB,WAAW,KAAK,IAAI,IAAI,wBAAwB,KAAK,KAAK;AAAA,MAC1D,QAAQ;AAAA,IACV,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,qBAAuC;AACrD,MAAI;AACF,QAAI,CAAC,WAAW,cAAc,KAAK,CAAC,WAAW,cAAc,EAAG,QAAO;AAEvE,UAAM,OAAO,KAAK,MAAM,aAAa,gBAAgB,OAAO,CAAC;AAC7D,QAAI,KAAK,IAAI,IAAI,KAAK,WAAW;AAC/B,yBAAmB;AACnB,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,QAAQ;AACf,YAAM,cAAc,aAAa,gBAAgB,OAAO;AACxD,YAAM,cAAc,WAAW,QAAQ,EAAE,OAAO,WAAW,EAAE,OAAO,KAAK;AACzE,UAAI,gBAAgB,KAAK,QAAQ;AAC/B,2BAAmB;AACnB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,UAAM,MAAM,UAAQ,cAAc;AAClC,WAAO,IAAI,gBAAgB;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,qBAA2B;AACzC,MAAI;AACF,QAAI,WAAW,cAAc,EAAG,YAAW,cAAc;AACzD,QAAI,WAAW,cAAc,EAAG,YAAW,cAAc;AAAA,EAC3D,QAAQ;AAAA,EAAe;AACzB;","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;
@@ -3001,13 +3003,13 @@ function renderSarifReport(result) {
3001
3003
  }
3002
3004
  },
3003
3005
  results: result.findings.map((f) => {
3004
- const entry = {
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 = execSync(`git diff --name-only ${base}`, { cwd: dir, encoding: "utf-8" }).trim().split("\n").filter(Boolean);
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
- 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(`
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.8.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-ZNWEMMEL.js");
3498
- const chalk4 = (await import("chalk")).default;
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(chalk4.yellow("Please log in first: xploitscan auth login"));
3620
+ console.log(chalk5.yellow("Please log in first: xploitscan auth login"));
3502
3621
  return;
3503
3622
  }
3504
- console.log(chalk4.cyan("Creating checkout session..."));
3623
+ console.log(chalk5.cyan("Creating checkout session..."));
3505
3624
  const url = await getCheckoutUrl();
3506
3625
  if (url) {
3507
- console.log(chalk4.green(`
3626
+ console.log(chalk5.green(`
3508
3627
  Open this URL to upgrade:`));
3509
- console.log(chalk4.bold.underline(url));
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(chalk4.red("Failed to create checkout session. Please try again."));
3634
+ console.log(chalk5.red("Failed to create checkout session. Please try again."));
3516
3635
  }
3517
3636
  });
3518
3637
  program.parse();