blitzstrike 1.0.40 → 1.0.43

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/index.js +436 -51
  3. package/package.json +12 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ All notable changes to Blitz Strike are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [1.0.43] — 2026-09-13
9
+
10
+ ### Added
11
+
12
+ - **Phase 8 — Production Hardening**:
13
+ - **Caching** (`src/cache.ts`, §46) — versioned result cache (`sha256(source) + engine_version`);
14
+ wired into `analyzeDataFlow2` so unchanged files skip re-parsing.
15
+ - **SARIF export** (`src/sarif.ts`, §32) — `toSarif()` emits a SARIF 2.1.0 document
16
+ (GitHub Code Scanning / GitLab / Azure DevOps).
17
+ - **Release validation** (`src/release.ts`, §49) — `validateRelease()` checks
18
+ package.json vs CHANGELOG (hard) + git tag / npm registry / lockfile (soft).
19
+ - **Dependency security** (`src/dependency.ts`, §50) — `checkDependencies()`
20
+ inspects install-time scripts and git/unusual dependency sources.
21
+ - **Incremental analysis** (`src/incremental.ts`, §47) — `analyzeChangedFiles()`
22
+ re-analyzes only the files a commit touched (git diff → changed files → engine).
23
+ - **CI/CD** (`.github/workflows/ci.yml`, §48) — typecheck, tests, build, npm pack,
24
+ release-consistency, and dependency-surface gates.
25
+ - `blitzstrike doctor` now reports release-metadata and dependency-surface health.
26
+
27
+ ## [1.0.42] — 2026-09-13
28
+
29
+ ### Added
30
+
31
+ - **Phase 7 — MCP Orchestration** (`src/orchestration.ts`, wired into `run_engagement`):
32
+ - A data-driven **state machine** (10 states: scope_check → plan → recon →
33
+ analyze → hypothesis → select_chain → validate → evidence → finding → report).
34
+ - A deterministic **planner** (`buildPlan`) that emits an ordered plan with
35
+ per-step tool, risk level, evidence requirements, and done/pending/blocked status.
36
+ - A **risk policy** (`actionPermitted`) that gates high-risk actions
37
+ (strike_verify, confirm_finding) behind scope.
38
+ - **§34 agent guidance** (`computeGuidance`) — every result now exposes
39
+ current_state / confidence / blocking_reason / required_evidence /
40
+ recommended_next_action / recommended_tool.
41
+ - `run_engagement` output now includes `state`, `plan`, `guidance`, and
42
+ `risk_policy` (no new MCP tools added — the surface was deepened, not widened).
43
+
44
+ ## [1.0.41] — 2026-09-13
45
+
46
+ ### Added
47
+
48
+ - **Phase 6 — Browser Validation** (`src/browser.ts` + MCP tool `browser_validate`):
49
+ - Playwright-based validation of static-analysis hypotheses (not a scanner —
50
+ gated, "use only when necessary").
51
+ - Checks: `dom_xss` (marker-payload execution), `open_redirect` (unexpected
52
+ redirect), `auth_bypass` (missing auth enforcement), `csrf` (tokenless
53
+ state-changing form).
54
+ - Each check returns structured evidence (verdict + final_url + dom/console/
55
+ page-error signals) suitable for attaching to a finding.
56
+ - `playwright-core` is an **optional peer dependency**; `browser_validate`
57
+ degrades to a clear `unavailable` result when it is not installed.
58
+ - Hermetic browser test suite (`bun run test:browser`, 8 checks) against a
59
+ local vulnerable server.
60
+
8
61
  ## [1.0.40] — 2026-09-13
9
62
 
10
63
  ### Changed (review + spec alignment)
@@ -116,6 +169,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
116
169
  now use `\b` word boundaries, so `outer_exec(...)` is no longer misclassified
117
170
  as command execution.
118
171
 
172
+ ## [1.0.34] — 2026-09-13
173
+
174
+ ### Fixed (Phase 1 review)
175
+
176
+ - `hasSecrets` rewritten as `redactSecrets(text) !== text` — deterministic
177
+ (fixed the `g`-flag `.test()` state bug) and no false positive on already-redacted
178
+ content (`password=[REDACTED]` is no longer flagged).
179
+ - Wired `finding_updated` audit event into `transition()` / `rejectFinding()`.
180
+
119
181
  ## [1.0.33] — 2026-09-13
120
182
 
121
183
  ### Added
@@ -142,6 +204,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
142
204
  - Shared `src/version.ts` — single source of truth for the package version
143
205
  (removed the duplicate VERSION readers in `server.ts`).
144
206
 
207
+ ## [1.0.32] — 2026-09-13
208
+
209
+ ### Added
210
+
211
+ - Self-update: `blitzstrike update` CLI command + `check_update` MCP tool
212
+ (`src/update.ts` with semver-aware version comparison).
213
+
214
+ ## [1.0.31] — 2026-09-13
215
+
216
+ ### Changed
217
+
218
+ - npm publish pipeline debug (version bump only, no functional change).
219
+
220
+ ## [1.0.30] — 2026-09-13
221
+
222
+ ### Changed
223
+
224
+ - Removed source attribution from the attack-vector taxonomy data (first-party
225
+ content; `_comment` and notices stripped).
226
+
227
+ ## [1.0.29] — 2026-09-13
228
+
229
+ ### Added
230
+
231
+ - Attack-vector taxonomy (`intelligence/attack_vectors.json`, 34 categories /
232
+ 588 vectors) + `list_attack_vectors` + `attack_vectors` MCP tools.
233
+
234
+ ## [1.0.28] — 2026-09-13
235
+
236
+ ### Fixed
237
+
238
+ - Benchmark corpus writes to the OS temp directory (hermetic; no writes inside
239
+ the read-only package).
240
+
241
+ ## [1.0.27] — 2026-09-13
242
+
243
+ ### Added
244
+
245
+ - Documentation milestone: CHANGELOG / ROADMAP / README + docs metrics.
246
+
145
247
  ## [1.0.26] — 2026-09-13
146
248
 
147
249
  The first hardening milestone: the whole roadmap is implemented, measured, and
package/dist/index.js CHANGED
@@ -30238,6 +30238,87 @@ function rejectFinding(finding, status, reason, evidence) {
30238
30238
  };
30239
30239
  }
30240
30240
 
30241
+ // src/orchestration.ts
30242
+ var STATE_MACHINE = [
30243
+ { state: "scope_check", description: "Validate target against scope + risk policy.", recommended_tools: ["scope_check"], required_evidence: ["scope_decision"], next: ["plan"], confidence: "high" },
30244
+ { state: "plan", description: "Derive the engagement plan from surface + scope.", recommended_tools: ["list_chains"], required_evidence: ["plan_steps"], next: ["recon"], confidence: "high" },
30245
+ { state: "recon", description: "Map the attack surface (endpoints, tech, WAF).", recommended_tools: ["blitz_scan", "active_scan", "fofa_search", "detect_waf"], required_evidence: ["attack_surface"], next: ["analyze"], confidence: "medium" },
30246
+ { state: "analyze", description: "Trace source→sink flows (interprocedural taint).", recommended_tools: ["eagle_eye2", "blitz_scan", "blitz_file"], required_evidence: ["sink_hits", "taint_flow"], next: ["hypothesis"], confidence: "medium" },
30247
+ { state: "hypothesis", description: "Form defensible hypotheses (static finding).", recommended_tools: ["blitz_file", "enrich_scan"], required_evidence: ["static_finding"], next: ["select_chain"], blocking: "live_validation_required", confidence: "high" },
30248
+ { state: "select_chain", description: "Match the hypothesis to an escalation chain.", recommended_tools: ["list_chains", "read_playbook"], required_evidence: ["matched_chain"], next: ["validate"], confidence: "high" },
30249
+ { state: "validate", description: "Verify live with baseline + marker + negative control.", recommended_tools: ["strike_verify", "browser_validate"], required_evidence: ["baseline", "marker_reflection", "negative_control"], next: ["evidence"], confidence: "medium" },
30250
+ { state: "evidence", description: "Attach hashable, provenance-bound evidence.", recommended_tools: ["finding_attach_evidence"], required_evidence: ["evidence_artifact"], next: ["finding"], confidence: "high" },
30251
+ { state: "finding", description: "Confirm the finding (invariants enforced).", recommended_tools: ["confirm_finding"], required_evidence: ["confirmed_finding"], next: ["report"], confidence: "high" },
30252
+ { state: "report", description: "Produce a reproducible report.", recommended_tools: ["report", "security-report"], required_evidence: ["report"], next: [], confidence: "high" }
30253
+ ];
30254
+ function stateDefinition(state) {
30255
+ return STATE_MACHINE.find((s) => s.state === state);
30256
+ }
30257
+ var RISK_POLICY = [
30258
+ { action: "scope_check", risk: "low", requires_scope: false, destructive: false },
30259
+ { action: "blitz_scan", risk: "low", requires_scope: false, destructive: false },
30260
+ { action: "blitz_file", risk: "low", requires_scope: false, destructive: false },
30261
+ { action: "eagle_eye2", risk: "low", requires_scope: false, destructive: false },
30262
+ { action: "enrich_scan", risk: "low", requires_scope: false, destructive: false },
30263
+ { action: "detect_waf", risk: "low", requires_scope: false, destructive: false },
30264
+ { action: "fofa_search", risk: "low", requires_scope: false, destructive: false },
30265
+ { action: "list_chains", risk: "low", requires_scope: false, destructive: false },
30266
+ { action: "read_playbook", risk: "low", requires_scope: false, destructive: false },
30267
+ { action: "active_scan", risk: "medium", requires_scope: true, destructive: false },
30268
+ { action: "browser_validate", risk: "medium", requires_scope: true, destructive: false },
30269
+ { action: "strike_verify", risk: "high", requires_scope: true, destructive: false },
30270
+ { action: "confirm_finding", risk: "high", requires_scope: true, destructive: false }
30271
+ ];
30272
+ function riskOf(action) {
30273
+ return RISK_POLICY.find((r) => r.action === action) ?? null;
30274
+ }
30275
+ function actionPermitted(action, scopeAllowed, mode) {
30276
+ const rule = riskOf(action);
30277
+ if (!rule)
30278
+ return true;
30279
+ if (mode === "ctf" || mode === "offensive" || mode === "reverse-engineering")
30280
+ return true;
30281
+ if (rule.requires_scope && !scopeAllowed)
30282
+ return false;
30283
+ return true;
30284
+ }
30285
+ function buildPlan(scopeAllowed, mode, executedState) {
30286
+ const order = ["scope_check", "plan", "recon", "analyze", "hypothesis", "select_chain", "validate", "evidence", "finding", "report"];
30287
+ const executedIdx = order.indexOf(executedState);
30288
+ return order.map((state, i) => {
30289
+ const def = stateDefinition(state);
30290
+ const tool = def.recommended_tools[0] ?? "";
30291
+ const rule = riskOf(tool);
30292
+ const permitted = actionPermitted(tool, scopeAllowed, mode);
30293
+ const status = i <= executedIdx ? "done" : permitted ? "pending" : "blocked";
30294
+ return {
30295
+ order: i + 1,
30296
+ state,
30297
+ action: def.description,
30298
+ tool,
30299
+ evidence_required: def.required_evidence,
30300
+ risk: rule?.risk ?? "low",
30301
+ permitted,
30302
+ status
30303
+ };
30304
+ });
30305
+ }
30306
+ function computeGuidance(state, scopeAllowed) {
30307
+ const def = stateDefinition(state);
30308
+ const next = def.next[0];
30309
+ const nextDef = next ? stateDefinition(next) : null;
30310
+ const tool = nextDef?.recommended_tools[0] ?? "";
30311
+ const permitted = actionPermitted(tool, scopeAllowed, "bug-bounty");
30312
+ return {
30313
+ state,
30314
+ confidence: def.confidence,
30315
+ blocking_reason: !scopeAllowed && nextDef && riskOf(tool)?.requires_scope ? "scope_required" : def.blocking ?? null,
30316
+ required_evidence: def.required_evidence,
30317
+ recommended_next_action: nextDef?.description ?? "complete",
30318
+ recommended_tool: permitted ? tool : ""
30319
+ };
30320
+ }
30321
+
30241
30322
  // src/orchestrator.ts
30242
30323
  var _chainsCache = null;
30243
30324
  function loadChains() {
@@ -30451,6 +30532,17 @@ function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000,
30451
30532
  }));
30452
30533
  report.status = "COMPLETE";
30453
30534
  report.note = "All findings are HYPOTHESES until verified live with strike_verify. " + "Apply each chain's negative_control before reporting. Confidence reflects static evidence only.";
30535
+ const executedState = "select_chain";
30536
+ const scopeAllowed = report.scope?.allowed ?? false;
30537
+ const plan = buildPlan(scopeAllowed, mode, executedState);
30538
+ report.state = executedState;
30539
+ report.state_description = stateDefinition(executedState).description;
30540
+ report.plan = plan;
30541
+ report.guidance = computeGuidance(executedState, scopeAllowed);
30542
+ report.risk_policy = {
30543
+ note: "High-risk actions (strike_verify, confirm_finding) require scope + are never run server-side automatically.",
30544
+ gated: plan.filter((p) => !p.permitted).map((p) => p.tool)
30545
+ };
30454
30546
  if (remember) {
30455
30547
  let captured = 0;
30456
30548
  for (const mc of blitz.matched_chains) {
@@ -31803,6 +31895,29 @@ function entryPoints(g) {
31803
31895
  return entries;
31804
31896
  }
31805
31897
 
31898
+ // src/cache.ts
31899
+ import { createHash as createHash3 } from "node:crypto";
31900
+ var mem = new Map;
31901
+ var hits = 0;
31902
+ var misses = 0;
31903
+ function cacheKey(kind, source) {
31904
+ const h = createHash3("sha256").update(source).digest("hex").slice(0, 20);
31905
+ return `${kind}:${VERSION}:${h}`;
31906
+ }
31907
+ function getCache(kind, source) {
31908
+ const k = cacheKey(kind, source);
31909
+ if (mem.has(k)) {
31910
+ hits += 1;
31911
+ return mem.get(k);
31912
+ }
31913
+ misses += 1;
31914
+ return;
31915
+ }
31916
+ function setCache(kind, source, value) {
31917
+ mem.set(cacheKey(kind, source), value);
31918
+ return value;
31919
+ }
31920
+
31806
31921
  // src/eagle2.ts
31807
31922
  var parser3 = new import_php_parser3.Engine({
31808
31923
  parser: { extractDoc: false, php7: true },
@@ -32230,6 +32345,15 @@ function resolveFuncId(name, summaries) {
32230
32345
  return null;
32231
32346
  }
32232
32347
  function analyzeDataFlow2(path) {
32348
+ let cacheCode = "";
32349
+ try {
32350
+ cacheCode = readFileSync12(path, "utf8");
32351
+ } catch {}
32352
+ if (cacheCode) {
32353
+ const cached = getCache("eagle2", cacheCode);
32354
+ if (cached)
32355
+ return cached;
32356
+ }
32233
32357
  const cg = buildCallGraph(path);
32234
32358
  const entries = entryPoints(cg);
32235
32359
  const reachable = new Set;
@@ -32245,7 +32369,7 @@ function analyzeDataFlow2(path) {
32245
32369
  };
32246
32370
  let code;
32247
32371
  try {
32248
- code = readFileSync12(path, "utf8");
32372
+ code = cacheCode || readFileSync12(path, "utf8");
32249
32373
  } catch {
32250
32374
  return result;
32251
32375
  }
@@ -32354,6 +32478,8 @@ function analyzeDataFlow2(path) {
32354
32478
  queue.push({ funcId: calleeId, param: calleeParam, source: item.source, sourceLine: item.sourceLine, viaReturn: false });
32355
32479
  }
32356
32480
  }
32481
+ if (cacheCode)
32482
+ setCache("eagle2", cacheCode, result);
32357
32483
  return result;
32358
32484
  }
32359
32485
 
@@ -51040,6 +51166,162 @@ function scanFramework(code, frameworkId) {
51040
51166
  };
51041
51167
  }
51042
51168
 
51169
+ // src/browser.ts
51170
+ var chromiumCache = null;
51171
+ async function ensureChromium() {
51172
+ if (chromiumCache)
51173
+ return chromiumCache;
51174
+ try {
51175
+ chromiumCache = await import("playwright-core");
51176
+ return chromiumCache;
51177
+ } catch {
51178
+ return null;
51179
+ }
51180
+ }
51181
+ function unavailable(check, target, reason) {
51182
+ return {
51183
+ check,
51184
+ target: redactSecrets(target),
51185
+ verdict: "unavailable",
51186
+ detail: reason,
51187
+ evidence: { console_errors: [], page_errors: [] }
51188
+ };
51189
+ }
51190
+ function errorEvidence(check, target, e) {
51191
+ return {
51192
+ check,
51193
+ target: redactSecrets(target),
51194
+ verdict: "error",
51195
+ detail: e instanceof Error ? e.message : String(e),
51196
+ evidence: { console_errors: [], page_errors: [] }
51197
+ };
51198
+ }
51199
+ async function validateDomXss(target, payload = '<img src=x onerror="window.__blitz_xss__=true">') {
51200
+ const chr = await ensureChromium();
51201
+ if (!chr)
51202
+ return unavailable("dom_xss", target, "playwright-core not installed (optional peer dependency)");
51203
+ const browser = await chr.chromium.launch({ headless: true });
51204
+ try {
51205
+ const page = await browser.newPage();
51206
+ const consoleErrors = [];
51207
+ const pageErrors = [];
51208
+ page.on("console", (m) => {
51209
+ if (m.type() === "error")
51210
+ consoleErrors.push(m.text());
51211
+ });
51212
+ page.on("pageerror", (e) => pageErrors.push(String(e)));
51213
+ const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
51214
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
51215
+ await page.waitForTimeout(600);
51216
+ const marker = await page.evaluate(() => window.__blitz_xss__ === true);
51217
+ return {
51218
+ check: "dom_xss",
51219
+ target: redactSecrets(target),
51220
+ verdict: marker ? "confirmed" : "not_confirmed",
51221
+ detail: marker ? "payload executed (marker set)" : "payload did not execute (no marker)",
51222
+ evidence: { final_url: page.url(), dom_marker: marker, console_errors: consoleErrors, page_errors: pageErrors }
51223
+ };
51224
+ } catch (e) {
51225
+ return errorEvidence("dom_xss", target, e);
51226
+ } finally {
51227
+ await browser.close();
51228
+ }
51229
+ }
51230
+ async function validateOpenRedirect(target, payload) {
51231
+ const chr = await ensureChromium();
51232
+ if (!chr)
51233
+ return unavailable("open_redirect", target, "playwright-core not installed (optional peer dependency)");
51234
+ const browser = await chr.chromium.launch({ headless: true });
51235
+ try {
51236
+ const page = await browser.newPage();
51237
+ const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
51238
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
51239
+ await page.waitForTimeout(400);
51240
+ const finalUrl = page.url();
51241
+ const redirected = finalUrl !== url && finalUrl.includes(payload);
51242
+ return {
51243
+ check: "open_redirect",
51244
+ target: redactSecrets(target),
51245
+ verdict: redirected ? "confirmed" : "not_confirmed",
51246
+ detail: redirected ? `browser redirected to ${finalUrl}` : `no redirect to attacker URL (final: ${finalUrl})`,
51247
+ evidence: { final_url: finalUrl, console_errors: [], page_errors: [] }
51248
+ };
51249
+ } catch (e) {
51250
+ return errorEvidence("open_redirect", target, e);
51251
+ } finally {
51252
+ await browser.close();
51253
+ }
51254
+ }
51255
+ async function validateAuthBypass(target) {
51256
+ const chr = await ensureChromium();
51257
+ if (!chr)
51258
+ return unavailable("auth_bypass", target, "playwright-core not installed (optional peer dependency)");
51259
+ const browser = await chr.chromium.launch({ headless: true });
51260
+ try {
51261
+ const page = await browser.newPage();
51262
+ const resp = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
51263
+ const status = resp?.status() ?? 0;
51264
+ const finalUrl = page.url();
51265
+ const enforced = status === 401 || status === 403 || /login|signin|auth/.test(finalUrl);
51266
+ return {
51267
+ check: "auth_bypass",
51268
+ target: redactSecrets(target),
51269
+ verdict: enforced ? "not_confirmed" : "confirmed",
51270
+ detail: enforced ? `auth enforced (status ${status}, final ${finalUrl})` : `no auth enforcement observed (status ${status}, final ${finalUrl})`,
51271
+ evidence: { final_url: finalUrl, status, console_errors: [], page_errors: [] }
51272
+ };
51273
+ } catch (e) {
51274
+ return errorEvidence("auth_bypass", target, e);
51275
+ } finally {
51276
+ await browser.close();
51277
+ }
51278
+ }
51279
+ async function validateCsrf(target) {
51280
+ const chr = await ensureChromium();
51281
+ if (!chr)
51282
+ return unavailable("csrf", target, "playwright-core not installed (optional peer dependency)");
51283
+ const browser = await chr.chromium.launch({ headless: true });
51284
+ try {
51285
+ const page = await browser.newPage();
51286
+ await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
51287
+ const result = await page.evaluate(() => {
51288
+ const forms = Array.from(document.querySelectorAll("form"));
51289
+ const stateChanging = forms.filter((f) => /post/i.test(f.method) || f.method === "");
51290
+ if (stateChanging.length === 0)
51291
+ return { token: true, count: 0 };
51292
+ for (const f of stateChanging) {
51293
+ const token = f.querySelector('input[name*="csrf"], input[name*="token"], input[name*="_token"], input[name*="nonce"]');
51294
+ if (!token)
51295
+ return { token: false, count: stateChanging.length };
51296
+ }
51297
+ return { token: true, count: stateChanging.length };
51298
+ });
51299
+ return {
51300
+ check: "csrf",
51301
+ target: redactSecrets(target),
51302
+ verdict: result.token ? "not_confirmed" : "confirmed",
51303
+ detail: result.token ? `CSRF token present (${result.count} form(s))` : `state-changing form missing CSRF token (${result.count} form(s))`,
51304
+ evidence: { csrf_token_present: result.token, forms_count: result.count, console_errors: [], page_errors: [] }
51305
+ };
51306
+ } catch (e) {
51307
+ return errorEvidence("csrf", target, e);
51308
+ } finally {
51309
+ await browser.close();
51310
+ }
51311
+ }
51312
+ async function browserValidate(check, target, payload) {
51313
+ switch (check) {
51314
+ case "dom_xss":
51315
+ return validateDomXss(target, payload);
51316
+ case "open_redirect":
51317
+ return validateOpenRedirect(target, payload ?? "https://attacker.example/");
51318
+ case "auth_bypass":
51319
+ return validateAuthBypass(target);
51320
+ case "csrf":
51321
+ return validateCsrf(target);
51322
+ }
51323
+ }
51324
+
51043
51325
  // src/guidance.ts
51044
51326
  var SINK_DIRECTIVES = {
51045
51327
  sql_execution: "Validate the SQL injection live with strike_verify (use a unique MARKER and a NEGATIVE CONTROL that stays inert), or read_tool_manual for sqlmap. Only a reflected marker + inert control = confirmed.",
@@ -52843,6 +53125,18 @@ function createServer() {
52843
53125
  out.scan = scanFramework(code, framework);
52844
53126
  return { content: [{ type: "text", text: JSON.stringify(out) }] };
52845
53127
  });
53128
+ server.registerTool("browser_validate", {
53129
+ title: "Browser validation (Playwright)",
53130
+ description: "PHASE 6: validate a static-analysis HYPOTHESIS by driving a real headless Chromium. Checks: dom_xss, open_redirect, auth_bypass, csrf. Use only when necessary — to confirm a hypothesis with browser evidence, not as a first-pass scan. Requires playwright-core (optional peer dependency).",
53131
+ inputSchema: {
53132
+ check: _enum(["dom_xss", "open_redirect", "auth_bypass", "csrf"]).describe("Validation check to run"),
53133
+ target: string2().describe("Target URL (use {{payload}} placeholder for dom_xss/open_redirect injection)"),
53134
+ payload: string2().optional().describe("Payload to inject (defaults to an XSS marker for dom_xss)")
53135
+ }
53136
+ }, async ({ check, target, payload }) => {
53137
+ const result = await browserValidate(check, target, payload);
53138
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
53139
+ });
52846
53140
  server.registerTool("taint_tree", {
52847
53141
  title: "Whole-tree taint scan",
52848
53142
  description: "EAGLE-EYE: run inter-procedural taint analysis across an entire source tree.",
@@ -52996,12 +53290,89 @@ async function serve() {
52996
53290
  }
52997
53291
 
52998
53292
  // src/cli.ts
52999
- import { readFileSync as readFileSync18, existsSync as existsSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "node:fs";
53000
- import { join as join13, dirname as dirname2 } from "node:path";
53293
+ import { readFileSync as readFileSync20, existsSync as existsSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "node:fs";
53294
+ import { join as join15, dirname as dirname2 } from "node:path";
53001
53295
  import { homedir as homedir5 } from "node:os";
53002
- import { fileURLToPath as fileURLToPath9 } from "node:url";
53296
+ import { fileURLToPath as fileURLToPath11 } from "node:url";
53003
53297
  import { spawnSync as spawnSync3 } from "node:child_process";
53298
+
53299
+ // src/release.ts
53300
+ import { readFileSync as readFileSync18, existsSync as existsSync7 } from "node:fs";
53301
+ import { join as join13 } from "node:path";
53302
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
53303
+ import { execSync as execSync2 } from "node:child_process";
53004
53304
  var ROOT7 = join13(fileURLToPath9(new URL(".", import.meta.url)), "..");
53305
+ function validateRelease() {
53306
+ const checks = [];
53307
+ checks.push({ name: "package.json version", ok: true, detail: VERSION });
53308
+ try {
53309
+ const changelog = readFileSync18(join13(ROOT7, "CHANGELOG.md"), "utf8");
53310
+ const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
53311
+ const changelogVersion = m?.[1] ?? null;
53312
+ checks.push({
53313
+ name: "CHANGELOG version",
53314
+ ok: changelogVersion === VERSION,
53315
+ detail: changelogVersion ?? "no version header found"
53316
+ });
53317
+ } catch {
53318
+ checks.push({ name: "CHANGELOG version", ok: false, detail: "CHANGELOG.md not found" });
53319
+ }
53320
+ try {
53321
+ const tag = execSync2("git describe --tags --abbrev=0", { cwd: ROOT7, encoding: "utf8" }).trim();
53322
+ checks.push({ name: "Git tag", ok: tag === `v${VERSION}`, detail: tag, soft: true });
53323
+ } catch {
53324
+ checks.push({ name: "Git tag", ok: true, detail: "unavailable (no git)", soft: true });
53325
+ }
53326
+ try {
53327
+ const published = execSync2("npm view blitzstrike version", { encoding: "utf8", timeout: 8000 }).trim();
53328
+ checks.push({ name: "npm registry version", ok: published === VERSION, detail: published, soft: true });
53329
+ } catch {
53330
+ checks.push({ name: "npm registry version", ok: true, detail: "unavailable (offline)", soft: true });
53331
+ }
53332
+ checks.push({
53333
+ name: "Lockfile",
53334
+ ok: lockfilePresent(),
53335
+ detail: lockfilePresent() ? "present (bun.lock / package-lock.json)" : "missing",
53336
+ soft: true
53337
+ });
53338
+ return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
53339
+ }
53340
+ function lockfilePresent() {
53341
+ return existsSync7(join13(ROOT7, "package-lock.json")) || existsSync7(join13(ROOT7, "bun.lock")) || existsSync7(join13(ROOT7, "bun.lockb"));
53342
+ }
53343
+
53344
+ // src/dependency.ts
53345
+ import { readFileSync as readFileSync19 } from "node:fs";
53346
+ import { join as join14 } from "node:path";
53347
+ import { fileURLToPath as fileURLToPath10 } from "node:url";
53348
+ var ROOT8 = join14(fileURLToPath10(new URL(".", import.meta.url)), "..");
53349
+ function checkDependencies() {
53350
+ let pkg = {};
53351
+ try {
53352
+ pkg = JSON.parse(readFileSync19(join14(ROOT8, "package.json"), "utf8"));
53353
+ } catch {
53354
+ return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
53355
+ }
53356
+ const installScriptNames = ["preinstall", "install", "postinstall", "prepare"];
53357
+ const installScripts = installScriptNames.filter((n) => pkg.scripts?.[n]).map((n) => ({ name: n, command: pkg.scripts[n] }));
53358
+ const allDeps = {
53359
+ ...pkg.dependencies ?? {},
53360
+ ...pkg.devDependencies ?? {},
53361
+ ...pkg.peerDependencies ?? {},
53362
+ ...pkg.optionalDependencies ?? {}
53363
+ };
53364
+ const gitDependencies = Object.entries(allDeps).filter(([, v]) => /^(git\+|git:|github:|gitlab:|bitbucket:|\w+:\/\/)/.test(v)).map(([name, v]) => `${name}@${v}`);
53365
+ const total = Object.keys(allDeps).length;
53366
+ return {
53367
+ install_scripts: installScripts,
53368
+ git_dependencies: gitDependencies,
53369
+ total_dependencies: total,
53370
+ safe: installScripts.length === 0 && gitDependencies.length === 0
53371
+ };
53372
+ }
53373
+
53374
+ // src/cli.ts
53375
+ var ROOT9 = join15(fileURLToPath11(new URL(".", import.meta.url)), "..");
53005
53376
  function which(cmd) {
53006
53377
  return spawnSync3(`command -v ${cmd}`, { shell: true }).status === 0;
53007
53378
  }
@@ -53036,13 +53407,27 @@ function runDoctor() {
53036
53407
  detail: fofa ? "set" : "FOFA_EMAIL/FOFA_KEY not set",
53037
53408
  fix: fofa ? undefined : "export FOFA_EMAIL=... && export FOFA_KEY=... (enables fofa_search)"
53038
53409
  });
53039
- const dataOk = existsSync7(join13(ROOT7, "chains.json")) && existsSync7(join13(ROOT7, "tools-catalog.json"));
53410
+ const dataOk = existsSync8(join15(ROOT9, "chains.json")) && existsSync8(join15(ROOT9, "tools-catalog.json"));
53040
53411
  issues.push({
53041
53412
  name: "Data layers (chains + tools-catalog)",
53042
53413
  status: dataOk ? "ok" : "fail",
53043
53414
  detail: dataOk ? "present" : "chains.json or tools-catalog.json missing",
53044
53415
  fix: dataOk ? undefined : "re-install blitzstrike"
53045
53416
  });
53417
+ const release = validateRelease();
53418
+ issues.push({
53419
+ name: "Release metadata",
53420
+ status: release.consistent ? "ok" : "fail",
53421
+ detail: release.consistent ? `v${release.version} consistent` : release.checks.filter((c) => !c.ok && !c.soft).map((c) => c.name).join(", ") || "drift",
53422
+ fix: release.consistent ? undefined : "align package.json version with the latest CHANGELOG entry"
53423
+ });
53424
+ const deps = checkDependencies();
53425
+ issues.push({
53426
+ name: "Dependency surface",
53427
+ status: deps.safe ? "ok" : "warn",
53428
+ detail: deps.safe ? `${deps.total_dependencies} deps, no install scripts` : `${deps.install_scripts.length} install script(s) + ${deps.git_dependencies.length} git dep(s)`,
53429
+ fix: deps.safe ? undefined : "remove install-time scripts and git dependencies"
53430
+ });
53046
53431
  const symbol = { ok: "OK ", warn: "WARN", fail: "FAIL" };
53047
53432
  for (const i of issues) {
53048
53433
  console.log(`${symbol[i.status]} ${i.name} ${i.detail}`);
@@ -53064,21 +53449,21 @@ BlitzStrike — ${verdict}`);
53064
53449
  function resolveCommand() {
53065
53450
  if (which("blitzstrike"))
53066
53451
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
53067
- const src = join13(ROOT7, "src", "index.ts");
53068
- if (which("bun") && existsSync7(src))
53452
+ const src = join15(ROOT9, "src", "index.ts");
53453
+ if (which("bun") && existsSync8(src))
53069
53454
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
53070
53455
  if (which("npx"))
53071
53456
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
53072
53457
  if (which("bunx"))
53073
53458
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
53074
- const dist = join13(ROOT7, "dist", "index.js");
53075
- if (existsSync7(dist))
53459
+ const dist = join15(ROOT9, "dist", "index.js");
53460
+ if (existsSync8(dist))
53076
53461
  return { command: "bun", args: [dist, "serve", "--mcp"] };
53077
53462
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
53078
53463
  }
53079
53464
  function readJson(p) {
53080
53465
  try {
53081
- return JSON.parse(readFileSync18(p, "utf8"));
53466
+ return JSON.parse(readFileSync20(p, "utf8"));
53082
53467
  } catch {
53083
53468
  return null;
53084
53469
  }
@@ -53086,7 +53471,7 @@ function readJson(p) {
53086
53471
  function jsonMcpServersWrite(p) {
53087
53472
  return (command, args) => {
53088
53473
  const dir = dirname2(p);
53089
- if (!existsSync7(dir))
53474
+ if (!existsSync8(dir))
53090
53475
  mkdirSync4(dir, { recursive: true });
53091
53476
  const existing = readJson(p) ?? {};
53092
53477
  existing.mcpServers = {
@@ -53099,7 +53484,7 @@ function jsonMcpServersWrite(p) {
53099
53484
  function opencodeWrite(p) {
53100
53485
  return (_command, _args) => {
53101
53486
  const dir = dirname2(p);
53102
- if (!existsSync7(dir))
53487
+ if (!existsSync8(dir))
53103
53488
  mkdirSync4(dir, { recursive: true });
53104
53489
  const existing = readJson(p) ?? {};
53105
53490
  existing.mcp = {
@@ -53111,26 +53496,26 @@ function opencodeWrite(p) {
53111
53496
  };
53112
53497
  }
53113
53498
  function copyOpenCodeAgents() {
53114
- const srcDir = join13(ROOT7, "opencode-agents");
53115
- const dstDir = join13(homedir5(), ".config", "opencode", "agents");
53116
- if (!existsSync7(srcDir))
53499
+ const srcDir = join15(ROOT9, "opencode-agents");
53500
+ const dstDir = join15(homedir5(), ".config", "opencode", "agents");
53501
+ if (!existsSync8(srcDir))
53117
53502
  return;
53118
- if (!existsSync7(dstDir))
53503
+ if (!existsSync8(dstDir))
53119
53504
  mkdirSync4(dstDir, { recursive: true });
53120
53505
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
53121
- const src = join13(srcDir, f);
53122
- const dst = join13(dstDir, f);
53123
- if (existsSync7(src)) {
53124
- writeFileSync5(dst, readFileSync18(src, "utf8"));
53506
+ const src = join15(srcDir, f);
53507
+ const dst = join15(dstDir, f);
53508
+ if (existsSync8(src)) {
53509
+ writeFileSync5(dst, readFileSync20(src, "utf8"));
53125
53510
  }
53126
53511
  }
53127
53512
  }
53128
53513
  function codexWrite(p) {
53129
53514
  return (command, args) => {
53130
53515
  const dir = dirname2(p);
53131
- if (!existsSync7(dir))
53516
+ if (!existsSync8(dir))
53132
53517
  mkdirSync4(dir, { recursive: true });
53133
- let existing = existsSync7(p) ? readFileSync18(p, "utf8") : "";
53518
+ let existing = existsSync8(p) ? readFileSync20(p, "utf8") : "";
53134
53519
  if (!existing.trimEnd().endsWith(`
53135
53520
  `))
53136
53521
  existing += `
@@ -53147,9 +53532,9 @@ args = ${JSON.stringify(args)}
53147
53532
  function hermesWrite(p) {
53148
53533
  return (command, args) => {
53149
53534
  const dir = dirname2(p);
53150
- if (!existsSync7(dir))
53535
+ if (!existsSync8(dir))
53151
53536
  mkdirSync4(dir, { recursive: true });
53152
- let existing = existsSync7(p) ? readFileSync18(p, "utf8") : "";
53537
+ let existing = existsSync8(p) ? readFileSync20(p, "utf8") : "";
53153
53538
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
53154
53539
  if (!existing.trimEnd().endsWith(`
53155
53540
  `))
@@ -53176,74 +53561,74 @@ mcp_servers:
53176
53561
  function detectAgents() {
53177
53562
  const home = homedir5();
53178
53563
  const agents = [];
53179
- const claudeJson = join13(home, ".claude.json");
53564
+ const claudeJson = join15(home, ".claude.json");
53180
53565
  agents.push({
53181
53566
  name: "Claude Code",
53182
53567
  detectPath: claudeJson,
53183
- installed: () => existsSync7(claudeJson) || which("claude"),
53568
+ installed: () => existsSync8(claudeJson) || which("claude"),
53184
53569
  write: jsonMcpServersWrite(claudeJson)
53185
53570
  });
53186
- const claudeDesktop = join13(home, ".config", "Claude", "claude_desktop_config.json");
53571
+ const claudeDesktop = join15(home, ".config", "Claude", "claude_desktop_config.json");
53187
53572
  agents.push({
53188
53573
  name: "Claude Desktop",
53189
53574
  detectPath: claudeDesktop,
53190
- installed: () => existsSync7(claudeDesktop),
53575
+ installed: () => existsSync8(claudeDesktop),
53191
53576
  write: jsonMcpServersWrite(claudeDesktop)
53192
53577
  });
53193
- const cursor = join13(home, ".cursor", "mcp.json");
53578
+ const cursor = join15(home, ".cursor", "mcp.json");
53194
53579
  agents.push({
53195
53580
  name: "Cursor",
53196
53581
  detectPath: cursor,
53197
- installed: () => existsSync7(join13(home, ".cursor")) || which("cursor"),
53582
+ installed: () => existsSync8(join15(home, ".cursor")) || which("cursor"),
53198
53583
  write: jsonMcpServersWrite(cursor)
53199
53584
  });
53200
- const opencode = join13(home, ".config", "opencode", "opencode.jsonc");
53585
+ const opencode = join15(home, ".config", "opencode", "opencode.jsonc");
53201
53586
  agents.push({
53202
53587
  name: "OpenCode",
53203
53588
  detectPath: opencode,
53204
- installed: () => existsSync7(join13(home, ".config", "opencode")) || which("opencode"),
53589
+ installed: () => existsSync8(join15(home, ".config", "opencode")) || which("opencode"),
53205
53590
  write: opencodeWrite(opencode)
53206
53591
  });
53207
- const codex = join13(home, ".codex", "config.toml");
53592
+ const codex = join15(home, ".codex", "config.toml");
53208
53593
  agents.push({
53209
53594
  name: "Codex",
53210
53595
  detectPath: codex,
53211
- installed: () => existsSync7(join13(home, ".codex")) || which("codex"),
53596
+ installed: () => existsSync8(join15(home, ".codex")) || which("codex"),
53212
53597
  write: codexWrite(codex)
53213
53598
  });
53214
- const hermes = join13(home, ".hermes", "config.yaml");
53599
+ const hermes = join15(home, ".hermes", "config.yaml");
53215
53600
  agents.push({
53216
53601
  name: "Hermes",
53217
53602
  detectPath: hermes,
53218
- installed: () => existsSync7(join13(home, ".hermes")) || which("hermes"),
53603
+ installed: () => existsSync8(join15(home, ".hermes")) || which("hermes"),
53219
53604
  write: hermesWrite(hermes)
53220
53605
  });
53221
- const gemini = join13(home, ".gemini", "settings.json");
53606
+ const gemini = join15(home, ".gemini", "settings.json");
53222
53607
  agents.push({
53223
53608
  name: "Gemini CLI",
53224
53609
  detectPath: gemini,
53225
- installed: () => existsSync7(join13(home, ".gemini")) || which("gemini"),
53610
+ installed: () => existsSync8(join15(home, ".gemini")) || which("gemini"),
53226
53611
  write: jsonMcpServersWrite(gemini)
53227
53612
  });
53228
- const windsurf = join13(home, ".codeium", "windsurf", "mcp_config.json");
53613
+ const windsurf = join15(home, ".codeium", "windsurf", "mcp_config.json");
53229
53614
  agents.push({
53230
53615
  name: "Windsurf",
53231
53616
  detectPath: windsurf,
53232
- installed: () => existsSync7(join13(home, ".codeium")) || which("windsurf"),
53617
+ installed: () => existsSync8(join15(home, ".codeium")) || which("windsurf"),
53233
53618
  write: jsonMcpServersWrite(windsurf)
53234
53619
  });
53235
- const copilot = join13(home, ".copilot", "mcp.json");
53620
+ const copilot = join15(home, ".copilot", "mcp.json");
53236
53621
  agents.push({
53237
53622
  name: "Copilot",
53238
53623
  detectPath: copilot,
53239
- installed: () => existsSync7(join13(home, ".copilot")),
53624
+ installed: () => existsSync8(join15(home, ".copilot")),
53240
53625
  write: jsonMcpServersWrite(copilot)
53241
53626
  });
53242
- const cline = join13(home, ".cline", "mcp_settings.json");
53627
+ const cline = join15(home, ".cline", "mcp_settings.json");
53243
53628
  agents.push({
53244
53629
  name: "Cline",
53245
53630
  detectPath: cline,
53246
- installed: () => existsSync7(join13(home, ".cline")),
53631
+ installed: () => existsSync8(join15(home, ".cline")),
53247
53632
  write: jsonMcpServersWrite(cline)
53248
53633
  });
53249
53634
  return agents;
@@ -53287,15 +53672,15 @@ Registered with ${ok}/${installed.length} agent(s).`);
53287
53672
  }
53288
53673
 
53289
53674
  // src/index.ts
53290
- import { readFileSync as readFileSync19, existsSync as existsSync8 } from "node:fs";
53291
- import { join as join14 } from "node:path";
53292
- import { fileURLToPath as fileURLToPath10 } from "node:url";
53293
- var ROOT8 = join14(fileURLToPath10(new URL(".", import.meta.url)), "..");
53675
+ import { readFileSync as readFileSync21, existsSync as existsSync9 } from "node:fs";
53676
+ import { join as join16 } from "node:path";
53677
+ import { fileURLToPath as fileURLToPath12 } from "node:url";
53678
+ var ROOT10 = join16(fileURLToPath12(new URL(".", import.meta.url)), "..");
53294
53679
  function readVersion() {
53295
53680
  try {
53296
- const p = join14(ROOT8, "package.json");
53297
- if (existsSync8(p))
53298
- return JSON.parse(readFileSync19(p, "utf8")).version ?? "1.0.0";
53681
+ const p = join16(ROOT10, "package.json");
53682
+ if (existsSync9(p))
53683
+ return JSON.parse(readFileSync21(p, "utf8")).version ?? "1.0.0";
53299
53684
  } catch {}
53300
53685
  return "1.0.0";
53301
53686
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "1.0.40",
3
+ "version": "1.0.43",
4
4
  "description": "Blitz Strike — a universal MCP security-audit toolbelt. BLITZ sweeps the attack surface, EAGLE-EYE traces source-to-sink, STRIKE verifies live. 57 attack chains, 130-tool catalog, intelligence data layer. One server, every agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,12 +25,13 @@
25
25
  "tools-catalog.json"
26
26
  ],
27
27
  "scripts": {
28
- "build": "bun build src/index.ts --outdir dist --target node --format esm",
28
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --external playwright-core",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "serve": "bun run src/index.ts serve",
31
31
  "doctor": "bun run src/index.ts doctor",
32
32
  "compile": "bun build src/index.ts --compile --outfile dist/blitzstrike",
33
33
  "test": "bun run test/verify.ts",
34
+ "test:browser": "bun run test/browser-verify.ts",
34
35
  "prepublishOnly": "bun run typecheck && bun run build",
35
36
  "bench": "bun run test/verify.ts"
36
37
  },
@@ -74,6 +75,15 @@
74
75
  },
75
76
  "devDependencies": {
76
77
  "@types/node": "^22.20.2",
78
+ "playwright-core": "1.61.0",
77
79
  "typescript": "^5.5.0"
80
+ },
81
+ "peerDependencies": {
82
+ "playwright-core": ">=1.61.0"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "playwright-core": {
86
+ "optional": true
87
+ }
78
88
  }
79
89
  }