blitzstrike 1.0.40 → 1.0.42

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 +34 -0
  2. package/dist/index.js +260 -0
  3. package/package.json +12 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ 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.42] — 2026-09-13
9
+
10
+ ### Added
11
+
12
+ - **Phase 7 — MCP Orchestration** (`src/orchestration.ts`, wired into `run_engagement`):
13
+ - A data-driven **state machine** (10 states: scope_check → plan → recon →
14
+ analyze → hypothesis → select_chain → validate → evidence → finding → report).
15
+ - A deterministic **planner** (`buildPlan`) that emits an ordered plan with
16
+ per-step tool, risk level, evidence requirements, and done/pending/blocked status.
17
+ - A **risk policy** (`actionPermitted`) that gates high-risk actions
18
+ (strike_verify, confirm_finding) behind scope.
19
+ - **§34 agent guidance** (`computeGuidance`) — every result now exposes
20
+ current_state / confidence / blocking_reason / required_evidence /
21
+ recommended_next_action / recommended_tool.
22
+ - `run_engagement` output now includes `state`, `plan`, `guidance`, and
23
+ `risk_policy` (no new MCP tools added — the surface was deepened, not widened).
24
+
25
+ ## [1.0.41] — 2026-09-13
26
+
27
+ ### Added
28
+
29
+ - **Phase 6 — Browser Validation** (`src/browser.ts` + MCP tool `browser_validate`):
30
+ - Playwright-based validation of static-analysis hypotheses (not a scanner —
31
+ gated, "use only when necessary").
32
+ - Checks: `dom_xss` (marker-payload execution), `open_redirect` (unexpected
33
+ redirect), `auth_bypass` (missing auth enforcement), `csrf` (tokenless
34
+ state-changing form).
35
+ - Each check returns structured evidence (verdict + final_url + dom/console/
36
+ page-error signals) suitable for attaching to a finding.
37
+ - `playwright-core` is an **optional peer dependency**; `browser_validate`
38
+ degrades to a clear `unavailable` result when it is not installed.
39
+ - Hermetic browser test suite (`bun run test:browser`, 8 checks) against a
40
+ local vulnerable server.
41
+
8
42
  ## [1.0.40] — 2026-09-13
9
43
 
10
44
  ### Changed (review + spec alignment)
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) {
@@ -51040,6 +51132,162 @@ function scanFramework(code, frameworkId) {
51040
51132
  };
51041
51133
  }
51042
51134
 
51135
+ // src/browser.ts
51136
+ var chromiumCache = null;
51137
+ async function ensureChromium() {
51138
+ if (chromiumCache)
51139
+ return chromiumCache;
51140
+ try {
51141
+ chromiumCache = await import("playwright-core");
51142
+ return chromiumCache;
51143
+ } catch {
51144
+ return null;
51145
+ }
51146
+ }
51147
+ function unavailable(check, target, reason) {
51148
+ return {
51149
+ check,
51150
+ target: redactSecrets(target),
51151
+ verdict: "unavailable",
51152
+ detail: reason,
51153
+ evidence: { console_errors: [], page_errors: [] }
51154
+ };
51155
+ }
51156
+ function errorEvidence(check, target, e) {
51157
+ return {
51158
+ check,
51159
+ target: redactSecrets(target),
51160
+ verdict: "error",
51161
+ detail: e instanceof Error ? e.message : String(e),
51162
+ evidence: { console_errors: [], page_errors: [] }
51163
+ };
51164
+ }
51165
+ async function validateDomXss(target, payload = '<img src=x onerror="window.__blitz_xss__=true">') {
51166
+ const chr = await ensureChromium();
51167
+ if (!chr)
51168
+ return unavailable("dom_xss", target, "playwright-core not installed (optional peer dependency)");
51169
+ const browser = await chr.chromium.launch({ headless: true });
51170
+ try {
51171
+ const page = await browser.newPage();
51172
+ const consoleErrors = [];
51173
+ const pageErrors = [];
51174
+ page.on("console", (m) => {
51175
+ if (m.type() === "error")
51176
+ consoleErrors.push(m.text());
51177
+ });
51178
+ page.on("pageerror", (e) => pageErrors.push(String(e)));
51179
+ const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
51180
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
51181
+ await page.waitForTimeout(600);
51182
+ const marker = await page.evaluate(() => window.__blitz_xss__ === true);
51183
+ return {
51184
+ check: "dom_xss",
51185
+ target: redactSecrets(target),
51186
+ verdict: marker ? "confirmed" : "not_confirmed",
51187
+ detail: marker ? "payload executed (marker set)" : "payload did not execute (no marker)",
51188
+ evidence: { final_url: page.url(), dom_marker: marker, console_errors: consoleErrors, page_errors: pageErrors }
51189
+ };
51190
+ } catch (e) {
51191
+ return errorEvidence("dom_xss", target, e);
51192
+ } finally {
51193
+ await browser.close();
51194
+ }
51195
+ }
51196
+ async function validateOpenRedirect(target, payload) {
51197
+ const chr = await ensureChromium();
51198
+ if (!chr)
51199
+ return unavailable("open_redirect", target, "playwright-core not installed (optional peer dependency)");
51200
+ const browser = await chr.chromium.launch({ headless: true });
51201
+ try {
51202
+ const page = await browser.newPage();
51203
+ const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
51204
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
51205
+ await page.waitForTimeout(400);
51206
+ const finalUrl = page.url();
51207
+ const redirected = finalUrl !== url && finalUrl.includes(payload);
51208
+ return {
51209
+ check: "open_redirect",
51210
+ target: redactSecrets(target),
51211
+ verdict: redirected ? "confirmed" : "not_confirmed",
51212
+ detail: redirected ? `browser redirected to ${finalUrl}` : `no redirect to attacker URL (final: ${finalUrl})`,
51213
+ evidence: { final_url: finalUrl, console_errors: [], page_errors: [] }
51214
+ };
51215
+ } catch (e) {
51216
+ return errorEvidence("open_redirect", target, e);
51217
+ } finally {
51218
+ await browser.close();
51219
+ }
51220
+ }
51221
+ async function validateAuthBypass(target) {
51222
+ const chr = await ensureChromium();
51223
+ if (!chr)
51224
+ return unavailable("auth_bypass", target, "playwright-core not installed (optional peer dependency)");
51225
+ const browser = await chr.chromium.launch({ headless: true });
51226
+ try {
51227
+ const page = await browser.newPage();
51228
+ const resp = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
51229
+ const status = resp?.status() ?? 0;
51230
+ const finalUrl = page.url();
51231
+ const enforced = status === 401 || status === 403 || /login|signin|auth/.test(finalUrl);
51232
+ return {
51233
+ check: "auth_bypass",
51234
+ target: redactSecrets(target),
51235
+ verdict: enforced ? "not_confirmed" : "confirmed",
51236
+ detail: enforced ? `auth enforced (status ${status}, final ${finalUrl})` : `no auth enforcement observed (status ${status}, final ${finalUrl})`,
51237
+ evidence: { final_url: finalUrl, status, console_errors: [], page_errors: [] }
51238
+ };
51239
+ } catch (e) {
51240
+ return errorEvidence("auth_bypass", target, e);
51241
+ } finally {
51242
+ await browser.close();
51243
+ }
51244
+ }
51245
+ async function validateCsrf(target) {
51246
+ const chr = await ensureChromium();
51247
+ if (!chr)
51248
+ return unavailable("csrf", target, "playwright-core not installed (optional peer dependency)");
51249
+ const browser = await chr.chromium.launch({ headless: true });
51250
+ try {
51251
+ const page = await browser.newPage();
51252
+ await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
51253
+ const result = await page.evaluate(() => {
51254
+ const forms = Array.from(document.querySelectorAll("form"));
51255
+ const stateChanging = forms.filter((f) => /post/i.test(f.method) || f.method === "");
51256
+ if (stateChanging.length === 0)
51257
+ return { token: true, count: 0 };
51258
+ for (const f of stateChanging) {
51259
+ const token = f.querySelector('input[name*="csrf"], input[name*="token"], input[name*="_token"], input[name*="nonce"]');
51260
+ if (!token)
51261
+ return { token: false, count: stateChanging.length };
51262
+ }
51263
+ return { token: true, count: stateChanging.length };
51264
+ });
51265
+ return {
51266
+ check: "csrf",
51267
+ target: redactSecrets(target),
51268
+ verdict: result.token ? "not_confirmed" : "confirmed",
51269
+ detail: result.token ? `CSRF token present (${result.count} form(s))` : `state-changing form missing CSRF token (${result.count} form(s))`,
51270
+ evidence: { csrf_token_present: result.token, forms_count: result.count, console_errors: [], page_errors: [] }
51271
+ };
51272
+ } catch (e) {
51273
+ return errorEvidence("csrf", target, e);
51274
+ } finally {
51275
+ await browser.close();
51276
+ }
51277
+ }
51278
+ async function browserValidate(check, target, payload) {
51279
+ switch (check) {
51280
+ case "dom_xss":
51281
+ return validateDomXss(target, payload);
51282
+ case "open_redirect":
51283
+ return validateOpenRedirect(target, payload ?? "https://attacker.example/");
51284
+ case "auth_bypass":
51285
+ return validateAuthBypass(target);
51286
+ case "csrf":
51287
+ return validateCsrf(target);
51288
+ }
51289
+ }
51290
+
51043
51291
  // src/guidance.ts
51044
51292
  var SINK_DIRECTIVES = {
51045
51293
  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 +53091,18 @@ function createServer() {
52843
53091
  out.scan = scanFramework(code, framework);
52844
53092
  return { content: [{ type: "text", text: JSON.stringify(out) }] };
52845
53093
  });
53094
+ server.registerTool("browser_validate", {
53095
+ title: "Browser validation (Playwright)",
53096
+ 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).",
53097
+ inputSchema: {
53098
+ check: _enum(["dom_xss", "open_redirect", "auth_bypass", "csrf"]).describe("Validation check to run"),
53099
+ target: string2().describe("Target URL (use {{payload}} placeholder for dom_xss/open_redirect injection)"),
53100
+ payload: string2().optional().describe("Payload to inject (defaults to an XSS marker for dom_xss)")
53101
+ }
53102
+ }, async ({ check, target, payload }) => {
53103
+ const result = await browserValidate(check, target, payload);
53104
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
53105
+ });
52846
53106
  server.registerTool("taint_tree", {
52847
53107
  title: "Whole-tree taint scan",
52848
53108
  description: "EAGLE-EYE: run inter-procedural taint analysis across an entire source tree.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
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
  }