blitzstrike 2.4.6 → 2.4.8

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 +26 -0
  2. package/dist/index.js +281 -127
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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
+ ## [2.4.8] — 2026-09-14
9
+
10
+ ### Added — MCP integration health checks
11
+
12
+ - New `checkMcpServers` (`src/mcp-status.ts`): verifies external MCP
13
+ integrations are actually present/connected — chrome-devtools-mcp (stdio:
14
+ binary or npx auto-install) and burp-suite-mcp (SSE: alive only when Burp is
15
+ running with the extension loaded).
16
+ - `blitzstrike doctor` now reports both MCP servers with availability + fix hints.
17
+ - New MCP tool `check_mcp` to probe MCP integration status on demand.
18
+
19
+ ## [2.4.7] — 2026-09-14
20
+
21
+ ### Added — deterministic arbitrary-file-read verification (no bare hypotheses)
22
+
23
+ - New `verifyFileRead` (STRIKE): verifies a CWE-22 / path-traversal / LFI
24
+ hypothesis by reading a marker file (`/etc/passwd`) vs a non-existent
25
+ negative-control path, then comparing. Confirmed only when the marker returns
26
+ file content and the control does not — never from reasoning alone.
27
+ - Supports raw POST body (`bodyRaw`) and named query/body param injection.
28
+ - New MCP tool `verify_file_read` + chain hints `verify_file_read` /
29
+ `path_traversal` / `lfi` / `file_read` route to it; `strike_verify` now
30
+ auto-routes file-read findings to the file-read verifier.
31
+ - A gated endpoint (e.g. identical 500 auth wall) now returns an explicit
32
+ `unconfirmed` verdict with reason instead of leaving a bare hypothesis.
33
+
8
34
  ## [2.4.6] — 2026-09-14
9
35
 
10
36
  ### Added — chrome-devtools-mcp auto-install (npx zero-install fallback)
package/dist/index.js CHANGED
@@ -7667,6 +7667,95 @@ var init_catalog = __esm(() => {
7667
7667
  ROOT2 = join6(fileURLToPath3(new URL(".", import.meta.url)), "..");
7668
7668
  });
7669
7669
 
7670
+ // src/burp-mcp.ts
7671
+ async function fetchTimeout(url, init = {}, timeoutMs = 8000) {
7672
+ const ctrl = new AbortController;
7673
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
7674
+ try {
7675
+ return await fetch(url, { ...init, signal: ctrl.signal });
7676
+ } finally {
7677
+ clearTimeout(t);
7678
+ }
7679
+ }
7680
+ function sseMessage(text) {
7681
+ const m = text.match(/event:\s*message[\s\S]*?data:\s*([^\n]+)/);
7682
+ return m ? m[1].trim() : null;
7683
+ }
7684
+ async function driveBurpMCP(baseUrl = DEFAULT_BURP_URL, timeoutMs = 1e4) {
7685
+ const base = baseUrl.replace(/\/+$/, "");
7686
+ let sseRes;
7687
+ try {
7688
+ sseRes = await fetchTimeout(`${base}/sse`, { headers: { Accept: "text/event-stream" } }, timeoutMs);
7689
+ } catch (e) {
7690
+ return { ok: false, alive: false, base_url: base, error: `Burp MCP not reachable at ${base}/sse (${String(e)}) — start Burp + load the MCP extension` };
7691
+ }
7692
+ if (!sseRes.ok) {
7693
+ return {
7694
+ ok: false,
7695
+ alive: false,
7696
+ base_url: base,
7697
+ error: `Burp MCP SSE endpoint returned HTTP ${sseRes.status} — is the extension enabled in Burp → MCP?`
7698
+ };
7699
+ }
7700
+ let endpoint = null;
7701
+ let serverInfo = "";
7702
+ let toolsRaw = "";
7703
+ try {
7704
+ const reader = sseRes.body.getReader();
7705
+ const decoder = new TextDecoder;
7706
+ let buf = "";
7707
+ const deadline = Date.now() + timeoutMs;
7708
+ while (Date.now() < deadline) {
7709
+ const { value, done } = await reader.read();
7710
+ if (done)
7711
+ break;
7712
+ buf += decoder.decode(value, { stream: true });
7713
+ if (!endpoint) {
7714
+ const em = buf.match(/event:\s*endpoint[\s\S]*?data:\s*([^\n]+)/);
7715
+ if (em)
7716
+ endpoint = em[1].trim();
7717
+ }
7718
+ if (endpoint)
7719
+ break;
7720
+ }
7721
+ if (endpoint) {
7722
+ const postUrl = new URL(endpoint, base).toString();
7723
+ const initRes = await fetchTimeout(postUrl, {
7724
+ method: "POST",
7725
+ headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
7726
+ body: JSON.stringify({
7727
+ jsonrpc: "2.0",
7728
+ id: 1,
7729
+ method: "initialize",
7730
+ params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "blitzstrike", version: "2" } }
7731
+ })
7732
+ }, timeoutMs);
7733
+ const initText = await initRes.text();
7734
+ const initMsg = sseMessage(initText) ?? (initText.includes("result") ? initText : "");
7735
+ serverInfo = initMsg.slice(0, 600);
7736
+ const toolsRes = await fetchTimeout(postUrl, {
7737
+ method: "POST",
7738
+ headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
7739
+ body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })
7740
+ }, timeoutMs);
7741
+ const toolsText = await toolsRes.text();
7742
+ toolsRaw = (sseMessage(toolsText) ?? toolsText).slice(0, 3000);
7743
+ }
7744
+ } catch (e) {
7745
+ return { ok: true, alive: true, base_url: base, endpoint, error: `SSE stream opened but handshake failed: ${String(e)}` };
7746
+ }
7747
+ if (!endpoint) {
7748
+ return {
7749
+ ok: true,
7750
+ alive: true,
7751
+ base_url: base,
7752
+ error: "SSE stream open but no endpoint event received — Burp MCP extension may not be fully enabled"
7753
+ };
7754
+ }
7755
+ return { ok: true, alive: true, base_url: base, endpoint, server: serverInfo, tools: toolsRaw };
7756
+ }
7757
+ var DEFAULT_BURP_URL = "http://127.0.0.1:9876";
7758
+
7670
7759
  // node_modules/php-parser/src/lexer/attribute.js
7671
7760
  var require_attribute = __commonJS(function(exports, module) {
7672
7761
  module.exports = {
@@ -15295,12 +15384,39 @@ var require_src = __commonJS(function(exports, module) {
15295
15384
  module.exports.default = Engine;
15296
15385
  });
15297
15386
 
15387
+ // src/mcp-status.ts
15388
+ import { existsSync as existsSync8 } from "node:fs";
15389
+ import { spawnSync as spawnSync3 } from "node:child_process";
15390
+ async function checkMcpServers() {
15391
+ const out = [];
15392
+ const bin = ["/usr/bin/chrome-devtools-mcp", "/usr/local/bin/chrome-devtools-mcp"].find((p) => existsSync8(p));
15393
+ const npxOk = spawnSync3("npx", ["--version"], { timeout: 8000 }).error === undefined;
15394
+ const devtoolsOk = Boolean(bin) || npxOk;
15395
+ out.push({
15396
+ name: "chrome-devtools-mcp",
15397
+ available: devtoolsOk,
15398
+ kind: "stdio",
15399
+ detail: bin ? `installed at ${bin}` : npxOk ? "not installed — will auto-install via npx on first use" : "not installed (no npx either)",
15400
+ fix: devtoolsOk ? undefined : "install Node/npm, then `npm i -g chrome-devtools-mcp`"
15401
+ });
15402
+ const burp = await driveBurpMCP("http://127.0.0.1:9876", 4000);
15403
+ out.push({
15404
+ name: "burp-suite-mcp",
15405
+ available: burp.alive,
15406
+ kind: "sse",
15407
+ detail: burp.alive ? `alive at ${burp.base_url}${burp.endpoint ? ` (endpoint ${burp.endpoint})` : ""}` : burp.error ?? "not reachable",
15408
+ fix: burp.alive ? undefined : "build PortSwigger/mcp-server (`./gradlew embedProxyJar`) + load the JAR in Burp → Extensions, then enable the MCP extension"
15409
+ });
15410
+ return out;
15411
+ }
15412
+ var init_mcp_status = () => {};
15413
+
15298
15414
  // src/sync.ts
15299
15415
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync6, readFileSync as readFileSync18, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
15300
15416
  import { join as join16 } from "node:path";
15301
15417
  import { homedir as homedir5 } from "node:os";
15302
15418
  import { fileURLToPath as fileURLToPath9 } from "node:url";
15303
- import { spawnSync as spawnSync3 } from "node:child_process";
15419
+ import { spawnSync as spawnSync4 } from "node:child_process";
15304
15420
  function dirExists(p) {
15305
15421
  try {
15306
15422
  return statSync4(p).isDirectory();
@@ -15351,14 +15467,14 @@ function syncData() {
15351
15467
  copyDir(localTemplates, templatesPath);
15352
15468
  templatesSynced = true;
15353
15469
  }
15354
- const gitOk = spawnSync3("git", ["--version"]).status === 0;
15470
+ const gitOk = spawnSync4("git", ["--version"]).status === 0;
15355
15471
  if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
15356
15472
  const tmp = join16(DATA_ROOT2, ".tmp-clone");
15357
15473
  if (dirExists(tmp))
15358
15474
  rmSync4(tmp, { recursive: true, force: true });
15359
- const r = spawnSync3("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
15475
+ const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
15360
15476
  if (r.status === 0) {
15361
- const sparse = spawnSync3("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
15477
+ const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
15362
15478
  if (sparse.status === 0) {
15363
15479
  if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
15364
15480
  copyDir(join16(tmp, "payloads"), payloadsPath);
@@ -15379,7 +15495,7 @@ function syncData() {
15379
15495
  };
15380
15496
  }
15381
15497
  function copyDir(src, dst) {
15382
- const r = spawnSync3("cp", ["-r", src, dst], { timeout: 120000 });
15498
+ const r = spawnSync4("cp", ["-r", src, dst], { timeout: 120000 });
15383
15499
  if (r.status !== 0) {
15384
15500
  manualCopy(src, dst);
15385
15501
  }
@@ -15443,7 +15559,7 @@ var init_update = __esm(() => {
15443
15559
  });
15444
15560
 
15445
15561
  // src/release.ts
15446
- import { readFileSync as readFileSync20, existsSync as existsSync9 } from "node:fs";
15562
+ import { readFileSync as readFileSync20, existsSync as existsSync10 } from "node:fs";
15447
15563
  import { join as join17 } from "node:path";
15448
15564
  import { fileURLToPath as fileURLToPath10 } from "node:url";
15449
15565
  import { execSync as execSync4 } from "node:child_process";
@@ -15483,7 +15599,7 @@ function validateRelease() {
15483
15599
  return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
15484
15600
  }
15485
15601
  function lockfilePresent() {
15486
- return existsSync9(join17(ROOT8, "package-lock.json")) || existsSync9(join17(ROOT8, "bun.lock")) || existsSync9(join17(ROOT8, "bun.lockb"));
15602
+ return existsSync10(join17(ROOT8, "package-lock.json")) || existsSync10(join17(ROOT8, "bun.lock")) || existsSync10(join17(ROOT8, "bun.lockb"));
15487
15603
  }
15488
15604
  var ROOT8;
15489
15605
  var init_release = __esm(() => {
@@ -15525,20 +15641,20 @@ var init_dependency = __esm(() => {
15525
15641
  });
15526
15642
 
15527
15643
  // src/cli.ts
15528
- import { readFileSync as readFileSync22, existsSync as existsSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "node:fs";
15644
+ import { readFileSync as readFileSync22, existsSync as existsSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "node:fs";
15529
15645
  import { join as join19, dirname as dirname2 } from "node:path";
15530
15646
  import { homedir as homedir6 } from "node:os";
15531
15647
  import { fileURLToPath as fileURLToPath12 } from "node:url";
15532
- import { spawnSync as spawnSync4 } from "node:child_process";
15648
+ import { spawnSync as spawnSync5 } from "node:child_process";
15533
15649
  function which(cmd) {
15534
- return spawnSync4(`command -v ${cmd}`, { shell: true }).status === 0;
15650
+ return spawnSync5(`command -v ${cmd}`, { shell: true }).status === 0;
15535
15651
  }
15536
15652
  function installedCount() {
15537
15653
  const tools = loadTools();
15538
15654
  const installed = tools.filter((t) => which(t.name)).map((t) => t.name);
15539
15655
  return { total: tools.length, installed, missing: tools.map((t) => t.name).filter((n) => !installed.includes(n)) };
15540
15656
  }
15541
- function runDoctor() {
15657
+ async function runDoctor() {
15542
15658
  console.log(`BlitzStrike doctor — environment health check
15543
15659
  `);
15544
15660
  const issues = [];
@@ -15571,7 +15687,7 @@ function runDoctor() {
15571
15687
  detail: h1 ? `X-HackerOne-Research: ${h1}` : "H1_USERNAME not set",
15572
15688
  fix: h1 ? undefined : "export H1_USERNAME=<your-hackerone-username> (adds X-HackerOne-Research to outbound requests)"
15573
15689
  });
15574
- const dataOk = existsSync10(join19(ROOT10, "chains.json")) && existsSync10(join19(ROOT10, "tools-catalog.json"));
15690
+ const dataOk = existsSync11(join19(ROOT10, "chains.json")) && existsSync11(join19(ROOT10, "tools-catalog.json"));
15575
15691
  issues.push({
15576
15692
  name: "Data layers (chains + tools-catalog)",
15577
15693
  status: dataOk ? "ok" : "fail",
@@ -15592,6 +15708,15 @@ function runDoctor() {
15592
15708
  detail: deps.safe ? `${deps.total_dependencies} deps, no install scripts` : `${deps.install_scripts.length} install script(s) + ${deps.git_dependencies.length} git dep(s)`,
15593
15709
  fix: deps.safe ? undefined : "remove install-time scripts and git dependencies"
15594
15710
  });
15711
+ const mcps = await checkMcpServers();
15712
+ for (const m of mcps) {
15713
+ issues.push({
15714
+ name: `MCP: ${m.name}`,
15715
+ status: m.available ? "ok" : "warn",
15716
+ detail: m.detail,
15717
+ fix: m.fix
15718
+ });
15719
+ }
15595
15720
  const symbol = { ok: "OK ", warn: "WARN", fail: "FAIL" };
15596
15721
  for (const i of issues) {
15597
15722
  console.log(`${symbol[i.status]} ${i.name} ${i.detail}`);
@@ -15614,14 +15739,14 @@ function resolveCommand() {
15614
15739
  if (which("blitzstrike"))
15615
15740
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
15616
15741
  const src = join19(ROOT10, "src", "index.ts");
15617
- if (which("bun") && existsSync10(src))
15742
+ if (which("bun") && existsSync11(src))
15618
15743
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
15619
15744
  if (which("npx"))
15620
15745
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
15621
15746
  if (which("bunx"))
15622
15747
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
15623
15748
  const dist = join19(ROOT10, "dist", "index.js");
15624
- if (existsSync10(dist))
15749
+ if (existsSync11(dist))
15625
15750
  return { command: "bun", args: [dist, "serve", "--mcp"] };
15626
15751
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
15627
15752
  }
@@ -15635,7 +15760,7 @@ function readJson(p) {
15635
15760
  function jsonMcpServersWrite(p) {
15636
15761
  return (command, args) => {
15637
15762
  const dir = dirname2(p);
15638
- if (!existsSync10(dir))
15763
+ if (!existsSync11(dir))
15639
15764
  mkdirSync5(dir, { recursive: true });
15640
15765
  const existing = readJson(p) ?? {};
15641
15766
  existing.mcpServers = {
@@ -15648,7 +15773,7 @@ function jsonMcpServersWrite(p) {
15648
15773
  function opencodeWrite(p) {
15649
15774
  return (command, args) => {
15650
15775
  const dir = dirname2(p);
15651
- if (!existsSync10(dir))
15776
+ if (!existsSync11(dir))
15652
15777
  mkdirSync5(dir, { recursive: true });
15653
15778
  const existing = readJson(p) ?? {};
15654
15779
  existing.mcp = {
@@ -15662,14 +15787,14 @@ function opencodeWrite(p) {
15662
15787
  function copyOpenCodeAgents() {
15663
15788
  const srcDir = join19(ROOT10, "opencode-agents");
15664
15789
  const dstDir = join19(homedir6(), ".config", "opencode", "agents");
15665
- if (!existsSync10(srcDir))
15790
+ if (!existsSync11(srcDir))
15666
15791
  return;
15667
- if (!existsSync10(dstDir))
15792
+ if (!existsSync11(dstDir))
15668
15793
  mkdirSync5(dstDir, { recursive: true });
15669
15794
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
15670
15795
  const src = join19(srcDir, f);
15671
15796
  const dst = join19(dstDir, f);
15672
- if (existsSync10(src)) {
15797
+ if (existsSync11(src)) {
15673
15798
  writeFileSync7(dst, readFileSync22(src, "utf8"));
15674
15799
  }
15675
15800
  }
@@ -15677,9 +15802,9 @@ function copyOpenCodeAgents() {
15677
15802
  function codexWrite(p) {
15678
15803
  return (command, args) => {
15679
15804
  const dir = dirname2(p);
15680
- if (!existsSync10(dir))
15805
+ if (!existsSync11(dir))
15681
15806
  mkdirSync5(dir, { recursive: true });
15682
- let existing = existsSync10(p) ? readFileSync22(p, "utf8") : "";
15807
+ let existing = existsSync11(p) ? readFileSync22(p, "utf8") : "";
15683
15808
  if (!existing.trimEnd().endsWith(`
15684
15809
  `))
15685
15810
  existing += `
@@ -15696,9 +15821,9 @@ args = ${JSON.stringify(args)}
15696
15821
  function hermesWrite(p) {
15697
15822
  return (command, args) => {
15698
15823
  const dir = dirname2(p);
15699
- if (!existsSync10(dir))
15824
+ if (!existsSync11(dir))
15700
15825
  mkdirSync5(dir, { recursive: true });
15701
- let existing = existsSync10(p) ? readFileSync22(p, "utf8") : "";
15826
+ let existing = existsSync11(p) ? readFileSync22(p, "utf8") : "";
15702
15827
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
15703
15828
  if (!existing.trimEnd().endsWith(`
15704
15829
  `))
@@ -15729,70 +15854,70 @@ function detectAgents() {
15729
15854
  agents.push({
15730
15855
  name: "Claude Code",
15731
15856
  detectPath: claudeJson,
15732
- installed: () => existsSync10(claudeJson) || which("claude"),
15857
+ installed: () => existsSync11(claudeJson) || which("claude"),
15733
15858
  write: jsonMcpServersWrite(claudeJson)
15734
15859
  });
15735
15860
  const claudeDesktop = join19(home, ".config", "Claude", "claude_desktop_config.json");
15736
15861
  agents.push({
15737
15862
  name: "Claude Desktop",
15738
15863
  detectPath: claudeDesktop,
15739
- installed: () => existsSync10(claudeDesktop),
15864
+ installed: () => existsSync11(claudeDesktop),
15740
15865
  write: jsonMcpServersWrite(claudeDesktop)
15741
15866
  });
15742
15867
  const cursor = join19(home, ".cursor", "mcp.json");
15743
15868
  agents.push({
15744
15869
  name: "Cursor",
15745
15870
  detectPath: cursor,
15746
- installed: () => existsSync10(join19(home, ".cursor")) || which("cursor"),
15871
+ installed: () => existsSync11(join19(home, ".cursor")) || which("cursor"),
15747
15872
  write: jsonMcpServersWrite(cursor)
15748
15873
  });
15749
15874
  const opencode = join19(home, ".config", "opencode", "opencode.jsonc");
15750
15875
  agents.push({
15751
15876
  name: "OpenCode",
15752
15877
  detectPath: opencode,
15753
- installed: () => existsSync10(join19(home, ".config", "opencode")) || which("opencode"),
15878
+ installed: () => existsSync11(join19(home, ".config", "opencode")) || which("opencode"),
15754
15879
  write: opencodeWrite(opencode)
15755
15880
  });
15756
15881
  const codex = join19(home, ".codex", "config.toml");
15757
15882
  agents.push({
15758
15883
  name: "Codex",
15759
15884
  detectPath: codex,
15760
- installed: () => existsSync10(join19(home, ".codex")) || which("codex"),
15885
+ installed: () => existsSync11(join19(home, ".codex")) || which("codex"),
15761
15886
  write: codexWrite(codex)
15762
15887
  });
15763
15888
  const hermes = join19(home, ".hermes", "config.yaml");
15764
15889
  agents.push({
15765
15890
  name: "Hermes",
15766
15891
  detectPath: hermes,
15767
- installed: () => existsSync10(join19(home, ".hermes")) || which("hermes"),
15892
+ installed: () => existsSync11(join19(home, ".hermes")) || which("hermes"),
15768
15893
  write: hermesWrite(hermes)
15769
15894
  });
15770
15895
  const gemini = join19(home, ".gemini", "settings.json");
15771
15896
  agents.push({
15772
15897
  name: "Gemini CLI",
15773
15898
  detectPath: gemini,
15774
- installed: () => existsSync10(join19(home, ".gemini")) || which("gemini"),
15899
+ installed: () => existsSync11(join19(home, ".gemini")) || which("gemini"),
15775
15900
  write: jsonMcpServersWrite(gemini)
15776
15901
  });
15777
15902
  const windsurf = join19(home, ".codeium", "windsurf", "mcp_config.json");
15778
15903
  agents.push({
15779
15904
  name: "Windsurf",
15780
15905
  detectPath: windsurf,
15781
- installed: () => existsSync10(join19(home, ".codeium")) || which("windsurf"),
15906
+ installed: () => existsSync11(join19(home, ".codeium")) || which("windsurf"),
15782
15907
  write: jsonMcpServersWrite(windsurf)
15783
15908
  });
15784
15909
  const copilot = join19(home, ".copilot", "mcp.json");
15785
15910
  agents.push({
15786
15911
  name: "Copilot",
15787
15912
  detectPath: copilot,
15788
- installed: () => existsSync10(join19(home, ".copilot")),
15913
+ installed: () => existsSync11(join19(home, ".copilot")),
15789
15914
  write: jsonMcpServersWrite(copilot)
15790
15915
  });
15791
15916
  const cline = join19(home, ".cline", "mcp_settings.json");
15792
15917
  agents.push({
15793
15918
  name: "Cline",
15794
15919
  detectPath: cline,
15795
- installed: () => existsSync10(join19(home, ".cline")),
15920
+ installed: () => existsSync11(join19(home, ".cline")),
15796
15921
  write: jsonMcpServersWrite(cline)
15797
15922
  });
15798
15923
  return agents;
@@ -15874,6 +15999,7 @@ var init_cli = __esm(() => {
15874
15999
  init_catalog();
15875
16000
  init_release();
15876
16001
  init_dependency();
16002
+ init_mcp_status();
15877
16003
  ROOT10 = join19(fileURLToPath12(new URL(".", import.meta.url)), "..");
15878
16004
  });
15879
16005
 
@@ -31291,6 +31417,53 @@ ${controlResp.body}` }]
31291
31417
  evidence
31292
31418
  };
31293
31419
  }
31420
+ function buildFileReadRequest(url, path, param, bodyRaw) {
31421
+ if (bodyRaw)
31422
+ return { url, data: path };
31423
+ return { url: inject(url, path, param ?? "path") };
31424
+ }
31425
+ async function verifyFileRead(input) {
31426
+ const method = input.method ?? "POST";
31427
+ const markerPath = input.markerPath ?? "/etc/passwd";
31428
+ const controlPath = `/bs-nonexistent-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
31429
+ const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31430
+ const markerReq = buildFileReadRequest(input.url, markerPath, input.param, input.bodyRaw);
31431
+ const controlReq = buildFileReadRequest(input.url, controlPath, input.param, input.bodyRaw);
31432
+ const markerResp = await httpRequest(markerReq.url, method, markerReq.data, input.headers, timeoutMs);
31433
+ const controlResp = await httpRequest(controlReq.url, method, controlReq.data, input.headers, timeoutMs);
31434
+ const passwdRe = /(^|\n)root:[x*!][^:]*:0:0:/m;
31435
+ const markerFile = passwdRe.test(markerResp.body);
31436
+ const controlFile = passwdRe.test(controlResp.body);
31437
+ let status;
31438
+ let reason;
31439
+ if (markerResp.status === 0 && controlResp.status === 0) {
31440
+ status = "blocked";
31441
+ reason = "both marker and control requests failed — target unreachable";
31442
+ } else if (markerFile && !controlFile) {
31443
+ status = "confirmed";
31444
+ reason = "marker file (/etc/passwd) content returned while the non-existent negative control did not — arbitrary file read confirmed";
31445
+ } else if (markerFile && controlFile) {
31446
+ status = "unconfirmed";
31447
+ reason = "marker and negative control both returned passwd-like content — indistinguishable from a fixed response";
31448
+ } else if (markerResp.status === controlResp.status && markerResp.body === controlResp.body) {
31449
+ status = "unconfirmed";
31450
+ reason = `marker and control returned identical responses (HTTP ${markerResp.status}) — likely an auth/session gate; file read not reachable without credentials`;
31451
+ } else {
31452
+ status = "unconfirmed";
31453
+ reason = "marker file content not returned — arbitrary file read could not be demonstrated live";
31454
+ }
31455
+ return {
31456
+ status,
31457
+ file_read: markerFile && !controlFile,
31458
+ marker_file_content: markerFile,
31459
+ control_file_content: controlFile,
31460
+ marker_status: markerResp.status,
31461
+ control_status: controlResp.status,
31462
+ marker_preview: redactSecrets(markerResp.body.slice(0, 300)),
31463
+ control_preview: redactSecrets(controlResp.body.slice(0, 300)),
31464
+ reason
31465
+ };
31466
+ }
31294
31467
  function resolveFinding(finding, verdict) {
31295
31468
  let f = finding;
31296
31469
  if (f.status === "hypothesis") {
@@ -31734,98 +31907,13 @@ async function driveChromeDevtools(target, timeoutMs = 25000) {
31734
31907
  }
31735
31908
  }
31736
31909
 
31737
- // src/burp-mcp.ts
31738
- var DEFAULT_BURP_URL = "http://127.0.0.1:9876";
31739
- async function fetchTimeout(url, init = {}, timeoutMs = 8000) {
31740
- const ctrl = new AbortController;
31741
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
31742
- try {
31743
- return await fetch(url, { ...init, signal: ctrl.signal });
31744
- } finally {
31745
- clearTimeout(t);
31746
- }
31747
- }
31748
- function sseMessage(text) {
31749
- const m = text.match(/event:\s*message[\s\S]*?data:\s*([^\n]+)/);
31750
- return m ? m[1].trim() : null;
31751
- }
31752
- async function driveBurpMCP(baseUrl = DEFAULT_BURP_URL, timeoutMs = 1e4) {
31753
- const base = baseUrl.replace(/\/+$/, "");
31754
- let sseRes;
31755
- try {
31756
- sseRes = await fetchTimeout(`${base}/sse`, { headers: { Accept: "text/event-stream" } }, timeoutMs);
31757
- } catch (e) {
31758
- return { ok: false, alive: false, base_url: base, error: `Burp MCP not reachable at ${base}/sse (${String(e)}) — start Burp + load the MCP extension` };
31759
- }
31760
- if (!sseRes.ok) {
31761
- return {
31762
- ok: false,
31763
- alive: false,
31764
- base_url: base,
31765
- error: `Burp MCP SSE endpoint returned HTTP ${sseRes.status} — is the extension enabled in Burp → MCP?`
31766
- };
31767
- }
31768
- let endpoint = null;
31769
- let serverInfo = "";
31770
- let toolsRaw = "";
31771
- try {
31772
- const reader = sseRes.body.getReader();
31773
- const decoder = new TextDecoder;
31774
- let buf = "";
31775
- const deadline = Date.now() + timeoutMs;
31776
- while (Date.now() < deadline) {
31777
- const { value, done } = await reader.read();
31778
- if (done)
31779
- break;
31780
- buf += decoder.decode(value, { stream: true });
31781
- if (!endpoint) {
31782
- const em = buf.match(/event:\s*endpoint[\s\S]*?data:\s*([^\n]+)/);
31783
- if (em)
31784
- endpoint = em[1].trim();
31785
- }
31786
- if (endpoint)
31787
- break;
31788
- }
31789
- if (endpoint) {
31790
- const postUrl = new URL(endpoint, base).toString();
31791
- const initRes = await fetchTimeout(postUrl, {
31792
- method: "POST",
31793
- headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
31794
- body: JSON.stringify({
31795
- jsonrpc: "2.0",
31796
- id: 1,
31797
- method: "initialize",
31798
- params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "blitzstrike", version: "2" } }
31799
- })
31800
- }, timeoutMs);
31801
- const initText = await initRes.text();
31802
- const initMsg = sseMessage(initText) ?? (initText.includes("result") ? initText : "");
31803
- serverInfo = initMsg.slice(0, 600);
31804
- const toolsRes = await fetchTimeout(postUrl, {
31805
- method: "POST",
31806
- headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
31807
- body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })
31808
- }, timeoutMs);
31809
- const toolsText = await toolsRes.text();
31810
- toolsRaw = (sseMessage(toolsText) ?? toolsText).slice(0, 3000);
31811
- }
31812
- } catch (e) {
31813
- return { ok: true, alive: true, base_url: base, endpoint, error: `SSE stream opened but handshake failed: ${String(e)}` };
31814
- }
31815
- if (!endpoint) {
31816
- return {
31817
- ok: true,
31818
- alive: true,
31819
- base_url: base,
31820
- error: "SSE stream open but no endpoint event received — Burp MCP extension may not be fully enabled"
31821
- };
31822
- }
31823
- return { ok: true, alive: true, base_url: base, endpoint, server: serverInfo, tools: toolsRaw };
31824
- }
31825
-
31826
31910
  // src/chain-executor.ts
31827
31911
  var BLITZ_FINDING_TOOLS = {
31828
31912
  strike_verify: verifyFinding,
31913
+ verify_file_read: fileReadFinding,
31914
+ path_traversal: fileReadFinding,
31915
+ lfi: fileReadFinding,
31916
+ file_read: fileReadFinding,
31829
31917
  crack_hash: crackFinding,
31830
31918
  hashcat: crackFinding,
31831
31919
  hashid: crackFinding,
@@ -31859,9 +31947,15 @@ function findingText(f) {
31859
31947
  function findingUrl(f) {
31860
31948
  return (f.target?.host ?? "") + (f.target?.endpoint ?? "");
31861
31949
  }
31950
+ function isFileReadFinding(f) {
31951
+ const text = `${f.classification?.cwe ?? ""} ${f.classification?.cwe_name ?? ""} ${f.sink?.type ?? ""} ${f.chain?.id ?? ""} ${f.chain?.name ?? ""} ${f.title ?? ""}`.toLowerCase();
31952
+ return /(cwe-?22|path.?traversal|arbitrary.?file|file.?read|directory.?traversal|lfi|\btraversal\b)/.test(text);
31953
+ }
31862
31954
  async function verifyFinding(f) {
31863
31955
  const url = findingUrl(f);
31864
31956
  const param = f.source?.name ?? "q";
31957
+ if (isFileReadFinding(f))
31958
+ return fileReadFinding(f);
31865
31959
  const v = await strikeVerify({ url, method: "GET", param });
31866
31960
  return {
31867
31961
  status: v.status,
@@ -31871,6 +31965,16 @@ async function verifyFinding(f) {
31871
31965
  leaked_sinks: v.leaked_sinks ?? []
31872
31966
  };
31873
31967
  }
31968
+ async function fileReadFinding(f) {
31969
+ const url = findingUrl(f);
31970
+ if (!url)
31971
+ return { executed: false, note: "no target URL on finding" };
31972
+ const srcType = (f.source?.type ?? "").toLowerCase();
31973
+ const bodyRaw = /(body|post|raw)/.test(srcType);
31974
+ const method = bodyRaw ? "POST" : "GET";
31975
+ const verdict = await verifyFileRead({ url, method, bodyRaw, param: f.source?.name });
31976
+ return { verify_kind: "file_read", ...verdict };
31977
+ }
31874
31978
  async function crackFinding(f) {
31875
31979
  const text = findingText(f);
31876
31980
  const hashRe = /(\$2[aby]\$\d+\$[./A-Za-z0-9]{53}|[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64})/g;
@@ -55347,6 +55451,9 @@ function browserDiagnostics() {
55347
55451
  return { console_errors: [...consoleErrors], page_errors: [...pageErrors] };
55348
55452
  }
55349
55453
 
55454
+ // src/server.ts
55455
+ init_mcp_status();
55456
+
55350
55457
  // src/route-confusion.ts
55351
55458
  var SOURCE_TOKENS = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post)|getParameter\(|req\.(query|body|params)/;
55352
55459
  function lines(code) {
@@ -55921,7 +56028,7 @@ function runBenchmark() {
55921
56028
  }
55922
56029
 
55923
56030
  // src/enterprise-benchmark.ts
55924
- import { existsSync as existsSync8, readFileSync as readFileSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
56031
+ import { existsSync as existsSync9, readFileSync as readFileSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
55925
56032
  import { join as join15, resolve } from "node:path";
55926
56033
  var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
55927
56034
  var ENTERPRISE_FIXTURES = [
@@ -55981,7 +56088,7 @@ function detectFixture(dir) {
55981
56088
  function runEnterpriseBenchmark() {
55982
56089
  const fixtures = [];
55983
56090
  for (const fx of ENTERPRISE_FIXTURES) {
55984
- if (!existsSync8(fx.dir)) {
56091
+ if (!existsSync9(fx.dir)) {
55985
56092
  fixtures.push({ id: fx.id, available: false, detected: 0, expected: fx.expected_vulns, recall: 0 });
55986
56093
  continue;
55987
56094
  }
@@ -56279,6 +56386,45 @@ function createServer() {
56279
56386
  }]
56280
56387
  };
56281
56388
  });
56389
+ server.registerTool("verify_file_read", {
56390
+ title: "Verify arbitrary file read / path traversal (marker file vs negative control)",
56391
+ description: "STRIKE: deterministic verification of an arbitrary-file-read / path-traversal hypothesis (CWE-22). Reads a marker file (/etc/passwd) vs a non-existent negative-control path and compares: confirmed (marker returns file content, control does not) / unconfirmed (indistinguishable or gated by auth) / blocked (unreachable). Supports raw POST body (bodyRaw=true) or a named query/body param. Never leaves a bare hypothesis — always returns a verdict + reason.",
56392
+ inputSchema: {
56393
+ url: string2().describe("Target URL (e.g. the file-read endpoint)"),
56394
+ method: _enum(["GET", "POST"]).optional().describe("HTTP method (default POST)"),
56395
+ param: string2().optional().describe("Query/body param name that carries the path (default 'path')"),
56396
+ bodyRaw: boolean2().optional().describe("Treat the raw POST body as the filesystem path (default false)"),
56397
+ headers: string2().optional().describe("JSON object of extra headers"),
56398
+ markerPath: string2().optional().describe("Marker file to read (default /etc/passwd)"),
56399
+ timeout: number2().int().optional().describe("Timeout seconds (default 15)")
56400
+ }
56401
+ }, async ({ url, method, param, bodyRaw, headers, markerPath, timeout }) => {
56402
+ let hdrs = {};
56403
+ try {
56404
+ hdrs = headers ? JSON.parse(headers) : {};
56405
+ } catch {}
56406
+ const verdict = await verifyFileRead({
56407
+ url,
56408
+ method: method ?? "POST",
56409
+ param,
56410
+ bodyRaw: bodyRaw ?? false,
56411
+ headers: hdrs,
56412
+ markerPath,
56413
+ timeoutMs: timeout ? timeout * 1000 : undefined
56414
+ });
56415
+ return {
56416
+ content: [{
56417
+ type: "text",
56418
+ text: JSON.stringify({
56419
+ ...verdict,
56420
+ next_steps: [
56421
+ `Verdict: ${verdict.status} — ${verdict.reason}`,
56422
+ verdict.status === "confirmed" ? "Confirmed arbitrary file read. Record with finding_create + attach the marker/control evidence." : verdict.status === "blocked" ? "Blocked. Target unreachable — retry or re-check scope." : "Unconfirmed. The response is gated (auth/session) or the file is not read — obtain a low-priv session and re-run with a valid session, or mark the finding unconfirmed with this evidence."
56423
+ ]
56424
+ })
56425
+ }]
56426
+ };
56427
+ });
56282
56428
  server.registerTool("strike_resolve", {
56283
56429
  title: "Resolve a finding from a STRIKE verdict",
56284
56430
  description: "STRIKE+FINDINGS: take a canonical Finding and a STRIKE verdict, and advance the lifecycle end-to-end — hypothesis->validating->confirmed (marker reflected, control inert) or ->false_positive/blocked/unconfirmed. Returns the updated finding with evidence + recomputed confidence.",
@@ -56574,6 +56720,14 @@ ${statusLine}` : statusLine },
56574
56720
  }, async ({ target }) => {
56575
56721
  return { content: [{ type: "text", text: JSON.stringify(await driveChromeDevtools(target)) }] };
56576
56722
  });
56723
+ server.registerTool("check_mcp", {
56724
+ title: "Check external MCP integrations (chrome-devtools-mcp + burp-suite-mcp)",
56725
+ description: "Verify every external MCP integration is actually present/connected: chrome-devtools-mcp (stdio — installed or npx auto-install) and burp-suite-mcp (SSE — only alive when Burp is running with the extension loaded). Returns availability + fix hints, so you know before you need them.",
56726
+ inputSchema: {}
56727
+ }, async () => {
56728
+ const status = await checkMcpServers();
56729
+ return { content: [{ type: "text", text: JSON.stringify({ mcp_servers: status }) }] };
56730
+ });
56577
56731
  server.registerTool("install_all_tools", {
56578
56732
  title: "Install every catalog tool at once",
56579
56733
  description: "CATALOG: install every non-MCP-server catalog tool in parallel (bounded concurrency), skipping MCP servers (they are connected, not installed). Returns a summary (installed / already_installed / failed) + per-tool result. Optionally filter by category (recon, web, api, cloud, system, ...). Use this to mass-provision the toolbelt instead of ensure_tool one-by-one.",
@@ -57517,14 +57671,14 @@ async function serve() {
57517
57671
 
57518
57672
  // src/index.ts
57519
57673
  init_cli();
57520
- import { readFileSync as readFileSync23, existsSync as existsSync11 } from "node:fs";
57674
+ import { readFileSync as readFileSync23, existsSync as existsSync12 } from "node:fs";
57521
57675
  import { join as join20 } from "node:path";
57522
57676
  import { fileURLToPath as fileURLToPath13 } from "node:url";
57523
57677
  var ROOT11 = join20(fileURLToPath13(new URL(".", import.meta.url)), "..");
57524
57678
  function readVersion() {
57525
57679
  try {
57526
57680
  const p = join20(ROOT11, "package.json");
57527
- if (existsSync11(p))
57681
+ if (existsSync12(p))
57528
57682
  return JSON.parse(readFileSync23(p, "utf8")).version ?? "1.0.0";
57529
57683
  } catch {}
57530
57684
  return "1.0.0";
@@ -57556,7 +57710,7 @@ async function main() {
57556
57710
  return;
57557
57711
  }
57558
57712
  if (args[0] === "doctor") {
57559
- runDoctor();
57713
+ await runDoctor();
57560
57714
  return;
57561
57715
  }
57562
57716
  if (args[0] === "install") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
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": {