blitzstrike 2.4.21 → 2.4.23

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ 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.23] — 2026-09-14
9
+
10
+ ### Precision — false-positive reduction + per-detector benchmark
11
+
12
+ - Fixed 6 false-positive patterns in the heuristic detectors (complex-bugs +
13
+ route-confusion): canonicalized include (`basename`/`realpath`), whitelist
14
+ lookup (`$allowed[$p]`), batch-forwarding WITH an auth re-check, anchored
15
+ validation regex (`preg_match('/^[a-z]+$/')`), constrained SSRF (fixed
16
+ `scheme://` host literal), and URL literals in the path-confusion detector.
17
+ - Extended the benchmark to measure per-detector precision/recall/F1 (taint +
18
+ complex_bugs + route_confusion) with 28 new labelled fixtures.
19
+ - Corpus now at 137 cases: **precision 1.0 / recall 1.0 / F1 1.0, 0 FP, 0 FN**
20
+ across all three detectors.
21
+
22
+ ## [2.4.22] — 2026-09-14
23
+
24
+ ### Added — on-disk report persistence (reports/ folder)
25
+
26
+ - `generate_report` now WRITES the HackerOne-grade markdown/JSON report to
27
+ `~/.blitzstrike/reports/<scope-slug>-<timestamp>.md` (override via
28
+ `BLITZSTRIKE_REPORT_DIR`) and returns the saved path — an engagement now
29
+ produces an on-disk deliverable, not just an in-memory string.
30
+ - `run_engagement` / `run_autonomous` also persist their inline report to the
31
+ same folder (report.report.saved).
32
+
8
33
  ## [2.4.21] — 2026-09-14
9
34
 
10
35
  ### Security — eliminate the last shell-interpolation sites
@@ -0,0 +1,9 @@
1
+ - id: cb-crlf-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ header("Location: /home");
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-crlf-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ header("Location: " . $_GET['url']);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-deser-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $data = json_decode($_GET['data']);
9
+ ```
@@ -0,0 +1,11 @@
1
+ - id: cb-deser-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $data = $_GET['data'];
9
+ $obj = unserialize($data);
10
+ class X { function __destruct() { system('id'); } }
11
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-massassign-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ extract($_POST, EXTR_SKIP);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-massassign-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ extract($_REQUEST);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-pathconf-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $f = basename($_GET['file']);
9
+ echo file_get_contents($f);
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-pathconf-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $f = $_GET['file'];
9
+ echo file_get_contents($f);
10
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-proto-safe
2
+ - language: javascript
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const x = Object.assign({}, { a: 1 });
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-proto-vuln
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const x = Object.assign({}, req.body);
8
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-ssrf-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ echo file_get_contents("https://api.example.com/" . $_GET['path']);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-ssrf-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $url = $_GET['url'];
9
+ echo file_get_contents($url);
10
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-ssti-safe
2
+ - language: javascript
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const html = _.template("<h1>static</h1>");
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: cb-ssti-vuln
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```js
7
+ const html = ejs.render(req.query.template);
8
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-typejuggle-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ if ($_GET['password'] === $secret) { echo 'ok'; }
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: cb-typejuggle-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ if ($_GET['password'] == $secret) { echo 'ok'; }
9
+ ```
@@ -0,0 +1,11 @@
1
+ - id: cb-xxe-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $xml = $_GET['xml'];
9
+ libxml_disable_entity_loader(true);
10
+ $doc = simplexml_load_string($xml);
11
+ ```
@@ -0,0 +1,10 @@
1
+ - id: cb-xxe-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: complex_bugs
5
+
6
+ ```php
7
+ <?php
8
+ $xml = $_GET['xml'];
9
+ $doc = simplexml_load_string($xml);
10
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-batch-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ foreach ($requests as $r) { if (!authorize($r)) continue; handle($r); }
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-batch-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ foreach ($requests as $r) { forward($r); }
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-dispatch-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $cb = $_GET['cb'];
9
+ call_user_func($map[$cb]);
10
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-dispatch-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ call_user_func($_GET['callback']);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-include-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $f = basename($_GET['f']);
9
+ include($f);
10
+ ```
@@ -0,0 +1,9 @@
1
+ - id: rc-include-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ include($_GET['page']);
9
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-method-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $m = $_GET['m'];
9
+ $obj->{$allowed[$m]}();
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-method-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $m = $_GET['m'];
9
+ $obj->$m();
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-normalize-safe
2
+ - language: php
3
+ - vulnerable: false
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $path = $_GET['p'];
9
+ if (preg_match("/^[a-z]+$/", $path)) route($path);
10
+ ```
@@ -0,0 +1,10 @@
1
+ - id: rc-normalize-vuln
2
+ - language: php
3
+ - vulnerable: true
4
+ - detector: route_confusion
5
+
6
+ ```php
7
+ <?php
8
+ $path = $_GET['p'];
9
+ if (preg_match("/^\/admin/", $path)) route($path);
10
+ ```
package/dist/index.js CHANGED
@@ -15493,9 +15493,9 @@ async function checkMcpServers() {
15493
15493
  var init_mcp_status = () => {};
15494
15494
 
15495
15495
  // src/sync.ts
15496
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6, readFileSync as readFileSync18, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
15497
- import { join as join16 } from "node:path";
15498
- import { homedir as homedir6 } from "node:os";
15496
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, readFileSync as readFileSync18, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
15497
+ import { join as join17 } from "node:path";
15498
+ import { homedir as homedir7 } from "node:os";
15499
15499
  import { fileURLToPath as fileURLToPath9 } from "node:url";
15500
15500
  import { spawnSync as spawnSync4 } from "node:child_process";
15501
15501
  function dirExists(p) {
@@ -15511,7 +15511,7 @@ function countFiles(dir) {
15511
15511
  const walk = (d) => {
15512
15512
  for (const e of readdirSync7(d, { withFileTypes: true })) {
15513
15513
  if (e.isDirectory())
15514
- walk(join16(d, e.name));
15514
+ walk(join17(d, e.name));
15515
15515
  else
15516
15516
  n++;
15517
15517
  }
@@ -15524,9 +15524,9 @@ function countFiles(dir) {
15524
15524
  }
15525
15525
  function syncData() {
15526
15526
  if (!dirExists(DATA_ROOT2))
15527
- mkdirSync5(DATA_ROOT2, { recursive: true });
15528
- const payloadsPath = join16(DATA_ROOT2, "payloads");
15529
- const templatesPath = join16(DATA_ROOT2, "templates");
15527
+ mkdirSync6(DATA_ROOT2, { recursive: true });
15528
+ const payloadsPath = join17(DATA_ROOT2, "payloads");
15529
+ const templatesPath = join17(DATA_ROOT2, "templates");
15530
15530
  const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
15531
15531
  const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
15532
15532
  if (hasPayloads && hasTemplates) {
@@ -15536,8 +15536,8 @@ function syncData() {
15536
15536
  data_root: DATA_ROOT2
15537
15537
  };
15538
15538
  }
15539
- const localPayloads = join16(ROOT7, "payloads");
15540
- const localTemplates = join16(ROOT7, "templates");
15539
+ const localPayloads = join17(ROOT7, "payloads");
15540
+ const localTemplates = join17(ROOT7, "templates");
15541
15541
  let payloadsSynced = false;
15542
15542
  let templatesSynced = false;
15543
15543
  if (!hasPayloads && dirExists(localPayloads)) {
@@ -15550,7 +15550,7 @@ function syncData() {
15550
15550
  }
15551
15551
  const gitOk = spawnSync4("git", ["--version"]).status === 0;
15552
15552
  if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
15553
- const tmp = join16(DATA_ROOT2, ".tmp-clone");
15553
+ const tmp = join17(DATA_ROOT2, ".tmp-clone");
15554
15554
  if (dirExists(tmp))
15555
15555
  rmSync4(tmp, { recursive: true, force: true });
15556
15556
  const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
@@ -15558,11 +15558,11 @@ function syncData() {
15558
15558
  const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
15559
15559
  if (sparse.status === 0) {
15560
15560
  if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
15561
- copyDir(join16(tmp, "payloads"), payloadsPath);
15561
+ copyDir(join17(tmp, "payloads"), payloadsPath);
15562
15562
  payloadsSynced = true;
15563
15563
  }
15564
15564
  if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
15565
- copyDir(join16(tmp, "templates"), templatesPath);
15565
+ copyDir(join17(tmp, "templates"), templatesPath);
15566
15566
  templatesSynced = true;
15567
15567
  }
15568
15568
  }
@@ -15582,20 +15582,20 @@ function copyDir(src, dst) {
15582
15582
  }
15583
15583
  }
15584
15584
  function manualCopy(src, dst) {
15585
- mkdirSync5(dst, { recursive: true });
15585
+ mkdirSync6(dst, { recursive: true });
15586
15586
  for (const e of readdirSync7(src, { withFileTypes: true })) {
15587
- const s = join16(src, e.name);
15588
- const d = join16(dst, e.name);
15587
+ const s = join17(src, e.name);
15588
+ const d = join17(dst, e.name);
15589
15589
  if (e.isDirectory())
15590
15590
  manualCopy(s, d);
15591
15591
  else
15592
- writeFileSync6(d, readFileSync18(s));
15592
+ writeFileSync7(d, readFileSync18(s));
15593
15593
  }
15594
15594
  }
15595
15595
  var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
15596
15596
  var init_sync = __esm(() => {
15597
- ROOT7 = join16(fileURLToPath9(new URL(".", import.meta.url)), "..");
15598
- DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join16(homedir6(), ".blitzstrike", "data");
15597
+ ROOT7 = join17(fileURLToPath9(new URL(".", import.meta.url)), "..");
15598
+ DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join17(homedir7(), ".blitzstrike", "data");
15599
15599
  });
15600
15600
 
15601
15601
  // src/update.ts
@@ -15641,14 +15641,14 @@ var init_update = __esm(() => {
15641
15641
 
15642
15642
  // src/release.ts
15643
15643
  import { readFileSync as readFileSync20, existsSync as existsSync10 } from "node:fs";
15644
- import { join as join17 } from "node:path";
15644
+ import { join as join18 } from "node:path";
15645
15645
  import { fileURLToPath as fileURLToPath10 } from "node:url";
15646
15646
  import { execSync as execSync2 } from "node:child_process";
15647
15647
  function validateRelease() {
15648
15648
  const checks = [];
15649
15649
  checks.push({ name: "package.json version", ok: true, detail: VERSION });
15650
15650
  try {
15651
- const changelog = readFileSync20(join17(ROOT8, "CHANGELOG.md"), "utf8");
15651
+ const changelog = readFileSync20(join18(ROOT8, "CHANGELOG.md"), "utf8");
15652
15652
  const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
15653
15653
  const changelogVersion = m?.[1] ?? null;
15654
15654
  checks.push({
@@ -15680,22 +15680,22 @@ function validateRelease() {
15680
15680
  return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
15681
15681
  }
15682
15682
  function lockfilePresent() {
15683
- return existsSync10(join17(ROOT8, "package-lock.json")) || existsSync10(join17(ROOT8, "bun.lock")) || existsSync10(join17(ROOT8, "bun.lockb"));
15683
+ return existsSync10(join18(ROOT8, "package-lock.json")) || existsSync10(join18(ROOT8, "bun.lock")) || existsSync10(join18(ROOT8, "bun.lockb"));
15684
15684
  }
15685
15685
  var ROOT8;
15686
15686
  var init_release = __esm(() => {
15687
15687
  init_version();
15688
- ROOT8 = join17(fileURLToPath10(new URL(".", import.meta.url)), "..");
15688
+ ROOT8 = join18(fileURLToPath10(new URL(".", import.meta.url)), "..");
15689
15689
  });
15690
15690
 
15691
15691
  // src/dependency.ts
15692
15692
  import { readFileSync as readFileSync21 } from "node:fs";
15693
- import { join as join18 } from "node:path";
15693
+ import { join as join19 } from "node:path";
15694
15694
  import { fileURLToPath as fileURLToPath11 } from "node:url";
15695
15695
  function checkDependencies() {
15696
15696
  let pkg = {};
15697
15697
  try {
15698
- pkg = JSON.parse(readFileSync21(join18(ROOT9, "package.json"), "utf8"));
15698
+ pkg = JSON.parse(readFileSync21(join19(ROOT9, "package.json"), "utf8"));
15699
15699
  } catch {
15700
15700
  return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
15701
15701
  }
@@ -15718,13 +15718,13 @@ function checkDependencies() {
15718
15718
  }
15719
15719
  var ROOT9;
15720
15720
  var init_dependency = __esm(() => {
15721
- ROOT9 = join18(fileURLToPath11(new URL(".", import.meta.url)), "..");
15721
+ ROOT9 = join19(fileURLToPath11(new URL(".", import.meta.url)), "..");
15722
15722
  });
15723
15723
 
15724
15724
  // src/cli.ts
15725
- import { readFileSync as readFileSync22, existsSync as existsSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "node:fs";
15726
- import { join as join19, dirname as dirname2 } from "node:path";
15727
- import { homedir as homedir7 } from "node:os";
15725
+ import { readFileSync as readFileSync22, existsSync as existsSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "node:fs";
15726
+ import { join as join20, dirname as dirname2 } from "node:path";
15727
+ import { homedir as homedir8 } from "node:os";
15728
15728
  import { fileURLToPath as fileURLToPath12 } from "node:url";
15729
15729
  import { spawnSync as spawnSync5 } from "node:child_process";
15730
15730
  function which(cmd) {
@@ -15768,7 +15768,7 @@ async function runDoctor() {
15768
15768
  detail: h1 ? `X-HackerOne-Research: ${h1}` : "H1_USERNAME not set",
15769
15769
  fix: h1 ? undefined : "export H1_USERNAME=<your-hackerone-username> (adds X-HackerOne-Research to outbound requests)"
15770
15770
  });
15771
- const dataOk = existsSync11(join19(ROOT10, "chains.json")) && existsSync11(join19(ROOT10, "tools-catalog.json"));
15771
+ const dataOk = existsSync11(join20(ROOT10, "chains.json")) && existsSync11(join20(ROOT10, "tools-catalog.json"));
15772
15772
  issues.push({
15773
15773
  name: "Data layers (chains + tools-catalog)",
15774
15774
  status: dataOk ? "ok" : "fail",
@@ -15819,14 +15819,14 @@ BlitzStrike — ${verdict}`);
15819
15819
  function resolveCommand() {
15820
15820
  if (which("blitzstrike"))
15821
15821
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
15822
- const src = join19(ROOT10, "src", "index.ts");
15822
+ const src = join20(ROOT10, "src", "index.ts");
15823
15823
  if (which("bun") && existsSync11(src))
15824
15824
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
15825
15825
  if (which("npx"))
15826
15826
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
15827
15827
  if (which("bunx"))
15828
15828
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
15829
- const dist = join19(ROOT10, "dist", "index.js");
15829
+ const dist = join20(ROOT10, "dist", "index.js");
15830
15830
  if (existsSync11(dist))
15831
15831
  return { command: "bun", args: [dist, "serve", "--mcp"] };
15832
15832
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
@@ -15842,41 +15842,41 @@ function jsonMcpServersWrite(p) {
15842
15842
  return (command, args) => {
15843
15843
  const dir = dirname2(p);
15844
15844
  if (!existsSync11(dir))
15845
- mkdirSync6(dir, { recursive: true });
15845
+ mkdirSync7(dir, { recursive: true });
15846
15846
  const existing = readJson(p) ?? {};
15847
15847
  existing.mcpServers = {
15848
15848
  ...existing.mcpServers ?? {},
15849
15849
  blitzstrike: { command, args }
15850
15850
  };
15851
- writeFileSync7(p, JSON.stringify(existing, null, 2));
15851
+ writeFileSync8(p, JSON.stringify(existing, null, 2));
15852
15852
  };
15853
15853
  }
15854
15854
  function opencodeWrite(p) {
15855
15855
  return (command, args) => {
15856
15856
  const dir = dirname2(p);
15857
15857
  if (!existsSync11(dir))
15858
- mkdirSync6(dir, { recursive: true });
15858
+ mkdirSync7(dir, { recursive: true });
15859
15859
  const existing = readJson(p) ?? {};
15860
15860
  existing.mcp = {
15861
15861
  ...existing.mcp ?? {},
15862
15862
  blitzstrike: { type: "local", command: [command, ...args], enabled: true }
15863
15863
  };
15864
- writeFileSync7(p, JSON.stringify(existing, null, 2));
15864
+ writeFileSync8(p, JSON.stringify(existing, null, 2));
15865
15865
  copyOpenCodeAgents();
15866
15866
  };
15867
15867
  }
15868
15868
  function copyOpenCodeAgents() {
15869
- const srcDir = join19(ROOT10, "opencode-agents");
15870
- const dstDir = join19(homedir7(), ".config", "opencode", "agents");
15869
+ const srcDir = join20(ROOT10, "opencode-agents");
15870
+ const dstDir = join20(homedir8(), ".config", "opencode", "agents");
15871
15871
  if (!existsSync11(srcDir))
15872
15872
  return;
15873
15873
  if (!existsSync11(dstDir))
15874
- mkdirSync6(dstDir, { recursive: true });
15874
+ mkdirSync7(dstDir, { recursive: true });
15875
15875
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
15876
- const src = join19(srcDir, f);
15877
- const dst = join19(dstDir, f);
15876
+ const src = join20(srcDir, f);
15877
+ const dst = join20(dstDir, f);
15878
15878
  if (existsSync11(src)) {
15879
- writeFileSync7(dst, readFileSync22(src, "utf8"));
15879
+ writeFileSync8(dst, readFileSync22(src, "utf8"));
15880
15880
  }
15881
15881
  }
15882
15882
  }
@@ -15884,7 +15884,7 @@ function codexWrite(p) {
15884
15884
  return (command, args) => {
15885
15885
  const dir = dirname2(p);
15886
15886
  if (!existsSync11(dir))
15887
- mkdirSync6(dir, { recursive: true });
15887
+ mkdirSync7(dir, { recursive: true });
15888
15888
  let existing = existsSync11(p) ? readFileSync22(p, "utf8") : "";
15889
15889
  if (!existing.trimEnd().endsWith(`
15890
15890
  `))
@@ -15896,14 +15896,14 @@ function codexWrite(p) {
15896
15896
  command = "${command}"
15897
15897
  args = ${JSON.stringify(args)}
15898
15898
  `;
15899
- writeFileSync7(p, existing);
15899
+ writeFileSync8(p, existing);
15900
15900
  };
15901
15901
  }
15902
15902
  function hermesWrite(p) {
15903
15903
  return (command, args) => {
15904
15904
  const dir = dirname2(p);
15905
15905
  if (!existsSync11(dir))
15906
- mkdirSync6(dir, { recursive: true });
15906
+ mkdirSync7(dir, { recursive: true });
15907
15907
  let existing = existsSync11(p) ? readFileSync22(p, "utf8") : "";
15908
15908
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
15909
15909
  if (!existing.trimEnd().endsWith(`
@@ -15925,80 +15925,80 @@ mcp_servers:
15925
15925
  args: ${JSON.stringify(args)}
15926
15926
  `;
15927
15927
  }
15928
- writeFileSync7(p, existing);
15928
+ writeFileSync8(p, existing);
15929
15929
  };
15930
15930
  }
15931
15931
  function detectAgents() {
15932
- const home = homedir7();
15932
+ const home = homedir8();
15933
15933
  const agents = [];
15934
- const claudeJson = join19(home, ".claude.json");
15934
+ const claudeJson = join20(home, ".claude.json");
15935
15935
  agents.push({
15936
15936
  name: "Claude Code",
15937
15937
  detectPath: claudeJson,
15938
15938
  installed: () => existsSync11(claudeJson) || which("claude"),
15939
15939
  write: jsonMcpServersWrite(claudeJson)
15940
15940
  });
15941
- const claudeDesktop = join19(home, ".config", "Claude", "claude_desktop_config.json");
15941
+ const claudeDesktop = join20(home, ".config", "Claude", "claude_desktop_config.json");
15942
15942
  agents.push({
15943
15943
  name: "Claude Desktop",
15944
15944
  detectPath: claudeDesktop,
15945
15945
  installed: () => existsSync11(claudeDesktop),
15946
15946
  write: jsonMcpServersWrite(claudeDesktop)
15947
15947
  });
15948
- const cursor = join19(home, ".cursor", "mcp.json");
15948
+ const cursor = join20(home, ".cursor", "mcp.json");
15949
15949
  agents.push({
15950
15950
  name: "Cursor",
15951
15951
  detectPath: cursor,
15952
- installed: () => existsSync11(join19(home, ".cursor")) || which("cursor"),
15952
+ installed: () => existsSync11(join20(home, ".cursor")) || which("cursor"),
15953
15953
  write: jsonMcpServersWrite(cursor)
15954
15954
  });
15955
- const opencode = join19(home, ".config", "opencode", "opencode.jsonc");
15955
+ const opencode = join20(home, ".config", "opencode", "opencode.jsonc");
15956
15956
  agents.push({
15957
15957
  name: "OpenCode",
15958
15958
  detectPath: opencode,
15959
- installed: () => existsSync11(join19(home, ".config", "opencode")) || which("opencode"),
15959
+ installed: () => existsSync11(join20(home, ".config", "opencode")) || which("opencode"),
15960
15960
  write: opencodeWrite(opencode)
15961
15961
  });
15962
- const codex = join19(home, ".codex", "config.toml");
15962
+ const codex = join20(home, ".codex", "config.toml");
15963
15963
  agents.push({
15964
15964
  name: "Codex",
15965
15965
  detectPath: codex,
15966
- installed: () => existsSync11(join19(home, ".codex")) || which("codex"),
15966
+ installed: () => existsSync11(join20(home, ".codex")) || which("codex"),
15967
15967
  write: codexWrite(codex)
15968
15968
  });
15969
- const hermes = join19(home, ".hermes", "config.yaml");
15969
+ const hermes = join20(home, ".hermes", "config.yaml");
15970
15970
  agents.push({
15971
15971
  name: "Hermes",
15972
15972
  detectPath: hermes,
15973
- installed: () => existsSync11(join19(home, ".hermes")) || which("hermes"),
15973
+ installed: () => existsSync11(join20(home, ".hermes")) || which("hermes"),
15974
15974
  write: hermesWrite(hermes)
15975
15975
  });
15976
- const gemini = join19(home, ".gemini", "settings.json");
15976
+ const gemini = join20(home, ".gemini", "settings.json");
15977
15977
  agents.push({
15978
15978
  name: "Gemini CLI",
15979
15979
  detectPath: gemini,
15980
- installed: () => existsSync11(join19(home, ".gemini")) || which("gemini"),
15980
+ installed: () => existsSync11(join20(home, ".gemini")) || which("gemini"),
15981
15981
  write: jsonMcpServersWrite(gemini)
15982
15982
  });
15983
- const windsurf = join19(home, ".codeium", "windsurf", "mcp_config.json");
15983
+ const windsurf = join20(home, ".codeium", "windsurf", "mcp_config.json");
15984
15984
  agents.push({
15985
15985
  name: "Windsurf",
15986
15986
  detectPath: windsurf,
15987
- installed: () => existsSync11(join19(home, ".codeium")) || which("windsurf"),
15987
+ installed: () => existsSync11(join20(home, ".codeium")) || which("windsurf"),
15988
15988
  write: jsonMcpServersWrite(windsurf)
15989
15989
  });
15990
- const copilot = join19(home, ".copilot", "mcp.json");
15990
+ const copilot = join20(home, ".copilot", "mcp.json");
15991
15991
  agents.push({
15992
15992
  name: "Copilot",
15993
15993
  detectPath: copilot,
15994
- installed: () => existsSync11(join19(home, ".copilot")),
15994
+ installed: () => existsSync11(join20(home, ".copilot")),
15995
15995
  write: jsonMcpServersWrite(copilot)
15996
15996
  });
15997
- const cline = join19(home, ".cline", "mcp_settings.json");
15997
+ const cline = join20(home, ".cline", "mcp_settings.json");
15998
15998
  agents.push({
15999
15999
  name: "Cline",
16000
16000
  detectPath: cline,
16001
- installed: () => existsSync11(join19(home, ".cline")),
16001
+ installed: () => existsSync11(join20(home, ".cline")),
16002
16002
  write: jsonMcpServersWrite(cline)
16003
16003
  });
16004
16004
  return agents;
@@ -16116,7 +16116,7 @@ var init_cli = __esm(() => {
16116
16116
  init_release();
16117
16117
  init_dependency();
16118
16118
  init_mcp_status();
16119
- ROOT10 = join19(fileURLToPath12(new URL(".", import.meta.url)), "..");
16119
+ ROOT10 = join20(fileURLToPath12(new URL(".", import.meta.url)), "..");
16120
16120
  });
16121
16121
 
16122
16122
  // node_modules/zod/v3/helpers/util.js
@@ -30838,7 +30838,7 @@ async function nvdLookup(cveId) {
30838
30838
  // src/orchestrator.ts
30839
30839
  init_scanner();
30840
30840
  import { readFileSync as readFileSync8, existsSync as existsSync6, statSync as statSync2 } from "node:fs";
30841
- import { join as join9 } from "node:path";
30841
+ import { join as join10 } from "node:path";
30842
30842
  import { fileURLToPath as fileURLToPath5 } from "node:url";
30843
30843
 
30844
30844
  // src/memory.ts
@@ -33115,6 +33115,9 @@ function cvssAssess(input) {
33115
33115
  }
33116
33116
 
33117
33117
  // src/report.ts
33118
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync4 } from "node:fs";
33119
+ import { join as join9 } from "node:path";
33120
+ import { homedir as homedir6 } from "node:os";
33118
33121
  var SEVERITY_METRICS = {
33119
33122
  critical: { AV: "N", AC: "L", PR: "N", UI: "N", S: "U", C: "H", I: "H", A: "H" },
33120
33123
  high: { AV: "N", AC: "L", PR: "L", UI: "N", S: "U", C: "H", I: "H", A: "N" },
@@ -33317,6 +33320,21 @@ function reportJson(findings, opts = {}) {
33317
33320
  };
33318
33321
  return JSON.stringify(payload, null, 2);
33319
33322
  }
33323
+ var REPORT_DIR = process.env.BLITZSTRIKE_REPORT_DIR ?? join9(homedir6(), ".blitzstrike", "reports");
33324
+ function slugify2(s) {
33325
+ const slug = s.toLowerCase().replace(/^https?:\/\//, "").replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/\./g, "_").slice(0, 60);
33326
+ return slug || "engagement";
33327
+ }
33328
+ function saveReport(content, opts = {}) {
33329
+ mkdirSync4(REPORT_DIR, { recursive: true });
33330
+ const slug = slugify2(opts.scope ?? opts.title ?? "engagement");
33331
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
33332
+ const ext = opts.format === "json" ? "json" : "md";
33333
+ const filename = `${slug}-${ts}.${ext}`;
33334
+ const path = join9(REPORT_DIR, filename);
33335
+ writeFileSync2(path, content, "utf8");
33336
+ return { path, dir: REPORT_DIR, filename };
33337
+ }
33320
33338
 
33321
33339
  // src/sarif.ts
33322
33340
  init_version();
@@ -33910,8 +33928,8 @@ function loadChains() {
33910
33928
  if (_chainsCache)
33911
33929
  return _chainsCache;
33912
33930
  try {
33913
- const dir = join9(fileURLToPath5(new URL(".", import.meta.url)), "..");
33914
- const path = join9(dir, "chains.json");
33931
+ const dir = join10(fileURLToPath5(new URL(".", import.meta.url)), "..");
33932
+ const path = join10(dir, "chains.json");
33915
33933
  const raw = JSON.parse(readFileSync8(path, "utf8"));
33916
33934
  _chainsCache = raw.chains ?? [];
33917
33935
  return _chainsCache;
@@ -34136,7 +34154,8 @@ function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000,
34136
34154
  summary: JSON.parse(reportJsonText).summary ?? null,
34137
34155
  markdown: reportMarkdownText,
34138
34156
  json: reportJsonText,
34139
- sarif: sarifDoc
34157
+ sarif: sarifDoc,
34158
+ saved: saveReport(reportMarkdownText, { title: reportTitle, scope: target, version: VERSION, format: "markdown" }).path
34140
34159
  };
34141
34160
  report.status = "DETECTED";
34142
34161
  report.note = "Detection complete — verification is the NEXT mandatory step (do not stop here). " + "Every finding is a HYPOTHESIS until verified: strike_verify (HTTP reflection, marker + negative control), " + "verify_file_read (CWE-22 / path traversal), or drive_devtools (DOM/JS precision). " + "Verify each finding NOW, then generate_report. Never deliver a report with unverified findings.";
@@ -34689,8 +34708,8 @@ var CHAIN_ID_TO_ESCALATION = {
34689
34708
  };
34690
34709
  function loadChainLinks() {
34691
34710
  try {
34692
- const dir = join9(fileURLToPath5(new URL(".", import.meta.url)), "..");
34693
- const data = JSON.parse(readFileSync8(join9(dir, "intelligence", "chain_links.json"), "utf8"));
34711
+ const dir = join10(fileURLToPath5(new URL(".", import.meta.url)), "..");
34712
+ const data = JSON.parse(readFileSync8(join10(dir, "intelligence", "chain_links.json"), "utf8"));
34694
34713
  return data.links ?? [];
34695
34714
  } catch {
34696
34715
  return [];
@@ -34846,14 +34865,14 @@ function compactState(phase, findings) {
34846
34865
  }
34847
34866
 
34848
34867
  // src/tasks.ts
34849
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
34850
- import { join as join10 } from "node:path";
34868
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "node:fs";
34869
+ import { join as join11 } from "node:path";
34851
34870
  import { fileURLToPath as fileURLToPath6 } from "node:url";
34852
34871
  function tasksDir() {
34853
- return join10(fileURLToPath6(new URL(".", import.meta.url)), "..", ".blitzstrike", "tasks");
34872
+ return join11(fileURLToPath6(new URL(".", import.meta.url)), "..", ".blitzstrike", "tasks");
34854
34873
  }
34855
34874
  function taskPath(id) {
34856
- return join10(tasksDir(), `${id}.json`);
34875
+ return join11(tasksDir(), `${id}.json`);
34857
34876
  }
34858
34877
  function load(id) {
34859
34878
  try {
@@ -34863,8 +34882,8 @@ function load(id) {
34863
34882
  }
34864
34883
  }
34865
34884
  function save(t) {
34866
- mkdirSync4(tasksDir(), { recursive: true });
34867
- writeFileSync2(taskPath(t.task_id), JSON.stringify(t, null, 2));
34885
+ mkdirSync5(tasksDir(), { recursive: true });
34886
+ writeFileSync3(taskPath(t.task_id), JSON.stringify(t, null, 2));
34868
34887
  }
34869
34888
  function taskStart(id, objective, plan) {
34870
34889
  const now = Date.now();
@@ -35161,8 +35180,8 @@ function engagementStatus() {
35161
35180
 
35162
35181
  // src/incremental.ts
35163
35182
  import { execFileSync as execFileSync2 } from "node:child_process";
35164
- import { writeFileSync as writeFileSync3, mkdtempSync, rmSync } from "node:fs";
35165
- import { join as join11 } from "node:path";
35183
+ import { writeFileSync as writeFileSync4, mkdtempSync, rmSync } from "node:fs";
35184
+ import { join as join12 } from "node:path";
35166
35185
  import { tmpdir } from "node:os";
35167
35186
 
35168
35187
  // src/eagle2.ts
@@ -54677,9 +54696,9 @@ function analyzeChangedFiles(repo, base = "HEAD~1", head = "HEAD") {
54677
54696
  continue;
54678
54697
  const lang = detectLanguage(file)?.language;
54679
54698
  if (lang === "php") {
54680
- const dir = mkdtempSync(join11(tmpdir(), "blitz-incr-"));
54681
- const tmp = join11(dir, "changed.php");
54682
- writeFileSync3(tmp, code);
54699
+ const dir = mkdtempSync(join12(tmpdir(), "blitz-incr-"));
54700
+ const tmp = join12(dir, "changed.php");
54701
+ writeFileSync4(tmp, code);
54683
54702
  try {
54684
54703
  const r = analyzeDataFlow2(tmp);
54685
54704
  for (const f of r.findings)
@@ -55091,16 +55110,16 @@ function taintTree(root, maxFiles = 2000) {
55091
55110
  }
55092
55111
 
55093
55112
  // src/differential.ts
55094
- import { writeFileSync as writeFileSync4, rmSync as rmSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14 } from "node:fs";
55095
- import { join as join12 } from "node:path";
55113
+ import { writeFileSync as writeFileSync5, rmSync as rmSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14 } from "node:fs";
55114
+ import { join as join13 } from "node:path";
55096
55115
  import { tmpdir as tmpdir2 } from "node:os";
55097
55116
  import { execFileSync as execFileSync3 } from "node:child_process";
55098
55117
  init_scanner();
55099
55118
  function runEngine(code, language, file) {
55100
55119
  if (language === "php") {
55101
- const dir = mkdtempSync2(join12(tmpdir2(), "blitz-diff-"));
55102
- const tmp = join12(dir, "old.php");
55103
- writeFileSync4(tmp, code);
55120
+ const dir = mkdtempSync2(join13(tmpdir2(), "blitz-diff-"));
55121
+ const tmp = join13(dir, "old.php");
55122
+ writeFileSync5(tmp, code);
55104
55123
  try {
55105
55124
  const r = analyzeDataFlow2(tmp);
55106
55125
  return r.findings.map((f) => ({ sink: f.sink, sink_line: f.sink_line, category: f.category, source: f.source, cwe: f.cwe }));
@@ -55285,15 +55304,15 @@ function analyzeGitDiff(repo, base = "HEAD~1", head = "HEAD") {
55285
55304
 
55286
55305
  // src/frameworks.ts
55287
55306
  import { readFileSync as readFileSync15, existsSync as existsSync7 } from "node:fs";
55288
- import { join as join13 } from "node:path";
55307
+ import { join as join14 } from "node:path";
55289
55308
  import { fileURLToPath as fileURLToPath7 } from "node:url";
55290
- var ROOT4 = join13(fileURLToPath7(new URL(".", import.meta.url)), "..");
55291
- var INTEL2 = join13(ROOT4, "intelligence");
55309
+ var ROOT4 = join14(fileURLToPath7(new URL(".", import.meta.url)), "..");
55310
+ var INTEL2 = join14(ROOT4, "intelligence");
55292
55311
  var cache = null;
55293
55312
  function load2() {
55294
55313
  if (cache)
55295
55314
  return cache;
55296
- const p = join13(INTEL2, "frameworks.json");
55315
+ const p = join14(INTEL2, "frameworks.json");
55297
55316
  if (!existsSync7(p))
55298
55317
  return null;
55299
55318
  try {
@@ -55611,6 +55630,8 @@ var DYNAMIC_METHOD_RE = /(->|::)\s*\$[A-Za-z_]/g;
55611
55630
  var BATCH_LOOP_RE = /\bforeach\s*\([^)]*(requests|batch|sub_requests|operations)\b/gi;
55612
55631
  var ROUTE_MATCH_RE = /\b(preg_match|preg_match_all|strpos|stripos|str_contains)\s*\(/i;
55613
55632
  var NORMALIZERS = /\b(strtolower|strtoupper|rtrim|urldecode|rawurldecode|parse_url|normalize|canonical)\b/i;
55633
+ var WHITELIST_LOOKUP = /\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/;
55634
+ var CANONICALIZE = /\b(basename|realpath|normalize|canonical|resolve|toRealPath)\s*\(/i;
55614
55635
  function detectDynamicDispatch(code, file, out) {
55615
55636
  const ls = lines(code);
55616
55637
  DYNAMIC_DISPATCH_RE.lastIndex = 0;
@@ -55622,6 +55643,8 @@ function detectDynamicDispatch(code, file, out) {
55622
55643
  `);
55623
55644
  if (!userControlled(l) && !userControlled(scope))
55624
55645
  continue;
55646
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
55647
+ continue;
55625
55648
  out.push({
55626
55649
  file,
55627
55650
  line: ln,
@@ -55644,6 +55667,8 @@ function detectDynamicMethod(code, file, out) {
55644
55667
  `);
55645
55668
  if (!userControlled(l) && !userControlled(scope))
55646
55669
  continue;
55670
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
55671
+ continue;
55647
55672
  out.push({
55648
55673
  file,
55649
55674
  line: ln,
@@ -55666,6 +55691,11 @@ function detectDynamicInclude(code, file, out) {
55666
55691
  `);
55667
55692
  if (!userControlled(l) && !userControlled(scope))
55668
55693
  continue;
55694
+ if (CANONICALIZE.test(scope + `
55695
+ ` + l))
55696
+ continue;
55697
+ if (WHITELIST_LOOKUP.test(l) || WHITELIST_LOOKUP.test(scope))
55698
+ continue;
55669
55699
  out.push({
55670
55700
  file,
55671
55701
  line: ln,
@@ -55688,14 +55718,16 @@ function detectBatchForwarding(code, file, out) {
55688
55718
  if (!forwards)
55689
55719
  continue;
55690
55720
  const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
55721
+ if (rechecks)
55722
+ continue;
55691
55723
  out.push({
55692
55724
  file,
55693
55725
  line: ln,
55694
55726
  type: "batch_forwarding",
55695
55727
  category: "Route confusion (batch/proxy forwarding)",
55696
- severity: rechecks ? "medium" : "high",
55728
+ severity: "high",
55697
55729
  evidence: lines(code)[ln - 1]?.trim() ?? "",
55698
- detail: rechecks ? "Batch/proxy loop forwards to inner handlers — auth is re-checked in scope, but verify it applies per-route." : "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
55730
+ detail: "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
55699
55731
  });
55700
55732
  }
55701
55733
  }
@@ -55711,6 +55743,8 @@ function detectRouteNormalization(code, file, out) {
55711
55743
  continue;
55712
55744
  if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
55713
55745
  continue;
55746
+ if (/preg_match\s*\(\s*["'][^"']*\^[^"']*\$[^"']*["']/.test(l))
55747
+ continue;
55714
55748
  out.push({
55715
55749
  file,
55716
55750
  line: i + 1,
@@ -55908,6 +55942,12 @@ function detectPathConfusion(code, file, out) {
55908
55942
  if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
55909
55943
  continue;
55910
55944
  if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
55945
+ ` + l))
55946
+ continue;
55947
+ if (/\$[A-Za-z_]\w*\s*\[\s*\$[A-Za-z_]\w*\s*\]/.test(scope + `
55948
+ ` + l))
55949
+ continue;
55950
+ if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
55911
55951
  ` + l))
55912
55952
  continue;
55913
55953
  out.push({
@@ -55933,6 +55973,9 @@ function detectSsrF(code, file, out) {
55933
55973
  if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
55934
55974
  continue;
55935
55975
  if (/(allowlist|whitelist|in_array|preg_match|parse_url|startsWith\(["']https?|endsWith\(["']|basename|realpath|normalize)/i.test(scope + `
55976
+ ` + l))
55977
+ continue;
55978
+ if (/["'][^"']*:\/\/[^"']*["']/.test(scope + `
55936
55979
  ` + l))
55937
55980
  continue;
55938
55981
  out.push({
@@ -56050,12 +56093,12 @@ function nextSteps(ctx) {
56050
56093
  }
56051
56094
 
56052
56095
  // src/benchmark.ts
56053
- import { readFileSync as readFileSync16, readdirSync as readdirSync5, writeFileSync as writeFileSync5, rmSync as rmSync3 } from "node:fs";
56054
- import { join as join14 } from "node:path";
56096
+ import { readFileSync as readFileSync16, readdirSync as readdirSync5, writeFileSync as writeFileSync6, rmSync as rmSync3 } from "node:fs";
56097
+ import { join as join15 } from "node:path";
56055
56098
  import { fileURLToPath as fileURLToPath8 } from "node:url";
56056
56099
  import { tmpdir as tmpdir3 } from "node:os";
56057
- var ROOT5 = join14(fileURLToPath8(new URL(".", import.meta.url)), "..");
56058
- var CORPUS = join14(ROOT5, "bench", "corpus");
56100
+ var ROOT5 = join15(fileURLToPath8(new URL(".", import.meta.url)), "..");
56101
+ var CORPUS = join15(ROOT5, "bench", "corpus");
56059
56102
  function parseCorpusFile(path) {
56060
56103
  let text;
56061
56104
  try {
@@ -56069,11 +56112,12 @@ function parseCorpusFile(path) {
56069
56112
  let language = "php";
56070
56113
  let vulnerable = true;
56071
56114
  let sinkType;
56115
+ let detector;
56072
56116
  let code = "";
56073
56117
  let inCode = false;
56074
56118
  let codeLang = "";
56075
56119
  for (const line of lines) {
56076
- const m = line.match(/^\s*-\s*(id|language|vulnerable|sink_type)\s*:\s*(.+)$/i);
56120
+ const m = line.match(/^\s*-\s*(id|language|vulnerable|sink_type|detector)\s*:\s*(.+)$/i);
56077
56121
  if (!inCode && m) {
56078
56122
  const key = m[1].toLowerCase();
56079
56123
  const val = m[2].trim();
@@ -56085,6 +56129,8 @@ function parseCorpusFile(path) {
56085
56129
  vulnerable = val === "true";
56086
56130
  else if (key === "sink_type")
56087
56131
  sinkType = val;
56132
+ else if (key === "detector")
56133
+ detector = val;
56088
56134
  continue;
56089
56135
  }
56090
56136
  const start = line.match(/^```(\w*)/);
@@ -56101,7 +56147,7 @@ function parseCorpusFile(path) {
56101
56147
  }
56102
56148
  if (!id)
56103
56149
  id = path.split("/").pop().replace(/\.md$/, "");
56104
- return { id, language: language || codeLang || "php", vulnerable, sink_type: sinkType, code: code.trim() };
56150
+ return { id, language: language || codeLang || "php", vulnerable, sink_type: sinkType, detector, code: code.trim() };
56105
56151
  }
56106
56152
  function loadCorpus() {
56107
56153
  let entries = [];
@@ -56109,7 +56155,7 @@ function loadCorpus() {
56109
56155
  for (const f of readdirSync5(CORPUS)) {
56110
56156
  if (!f.endsWith(".md"))
56111
56157
  continue;
56112
- const e = parseCorpusFile(join14(CORPUS, f));
56158
+ const e = parseCorpusFile(join15(CORPUS, f));
56113
56159
  if (e)
56114
56160
  entries.push(e);
56115
56161
  }
@@ -56119,10 +56165,22 @@ function loadCorpus() {
56119
56165
  return entries;
56120
56166
  }
56121
56167
  function detect(entry) {
56168
+ if (entry.detector === "route_confusion") {
56169
+ const f = detectRouteConfusion(entry.code, "bench." + entry.language);
56170
+ if (f.length > 0)
56171
+ return { detected: true, sinkKind: f[0].type };
56172
+ return { detected: false };
56173
+ }
56174
+ if (entry.detector === "complex_bugs") {
56175
+ const f = detectComplexBugs(entry.code, "bench." + entry.language);
56176
+ if (f.length > 0)
56177
+ return { detected: true, sinkKind: f[0].type };
56178
+ return { detected: false };
56179
+ }
56122
56180
  const lang = entry.language;
56123
56181
  if (lang === "php") {
56124
- const tmp = join14(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
56125
- writeFileSync5(tmp, entry.code);
56182
+ const tmp = join15(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
56183
+ writeFileSync6(tmp, entry.code);
56126
56184
  const r = analyzeDataFlow2(tmp);
56127
56185
  rmSync3(tmp, { force: true });
56128
56186
  if (r.findings.length > 0)
@@ -56150,6 +56208,19 @@ function runBenchmark() {
56150
56208
  const detectionRate = tp + fn > 0 ? tp / (tp + fn) : 0;
56151
56209
  const falsePositiveRate = fp + tn > 0 ? fp / (fp + tn) : 0;
56152
56210
  const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
56211
+ const byDetector = {};
56212
+ for (const r of results) {
56213
+ const d = r.entry.detector ?? "taint";
56214
+ byDetector[d] = byDetector[d] ?? { tp: 0, tn: 0, fp: 0, fn: 0, total: 0 };
56215
+ byDetector[d][r.outcome] += 1;
56216
+ byDetector[d].total += 1;
56217
+ }
56218
+ const detectorMetrics = Object.fromEntries(Object.entries(byDetector).map(([d, m]) => {
56219
+ const p = m.tp + m.fp > 0 ? m.tp / (m.tp + m.fp) : 0;
56220
+ const rec = m.tp + m.fn > 0 ? m.tp / (m.tp + m.fn) : 0;
56221
+ const f1 = p + rec > 0 ? 2 * p * rec / (p + rec) : 0;
56222
+ return [d, { ...m, precision: Number(p.toFixed(3)), recall: Number(rec.toFixed(3)), f1: Number(f1.toFixed(3)) }];
56223
+ }));
56153
56224
  return {
56154
56225
  total_cases: total,
56155
56226
  tp,
@@ -56159,21 +56230,22 @@ function runBenchmark() {
56159
56230
  detection_rate: Number(detectionRate.toFixed(3)),
56160
56231
  false_positive_rate: Number(falsePositiveRate.toFixed(3)),
56161
56232
  precision: Number(precision.toFixed(3)),
56162
- results: results.map((r) => ({ id: r.entry.id, language: r.entry.language, outcome: r.outcome, sink: r.sink_kind }))
56233
+ by_detector: detectorMetrics,
56234
+ results: results.map((r) => ({ id: r.entry.id, language: r.entry.language, detector: r.entry.detector ?? "taint", outcome: r.outcome, sink: r.sink_kind }))
56163
56235
  };
56164
56236
  }
56165
56237
 
56166
56238
  // src/enterprise-benchmark.ts
56167
56239
  import { existsSync as existsSync9, readFileSync as readFileSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
56168
- import { join as join15, resolve } from "node:path";
56240
+ import { join as join16, resolve } from "node:path";
56169
56241
  var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
56170
56242
  var ENTERPRISE_FIXTURES = [
56171
- { id: "php-laravel", dir: join15(ROOT6, "php-laravel"), expected_vulns: 4 },
56172
- { id: "js-express", dir: join15(ROOT6, "js-express"), expected_vulns: 2 },
56173
- { id: "py-django", dir: join15(ROOT6, "py-django"), expected_vulns: 2 },
56174
- { id: "java-spring", dir: join15(ROOT6, "java-spring"), expected_vulns: 2 },
56175
- { id: "wp-batch-route", dir: join15(ROOT6, "wp-batch-route"), expected_vulns: 1 },
56176
- { id: "php-complex", dir: join15(ROOT6, "php-complex"), expected_vulns: 4 }
56243
+ { id: "php-laravel", dir: join16(ROOT6, "php-laravel"), expected_vulns: 4 },
56244
+ { id: "js-express", dir: join16(ROOT6, "js-express"), expected_vulns: 2 },
56245
+ { id: "py-django", dir: join16(ROOT6, "py-django"), expected_vulns: 2 },
56246
+ { id: "java-spring", dir: join16(ROOT6, "java-spring"), expected_vulns: 2 },
56247
+ { id: "wp-batch-route", dir: join16(ROOT6, "wp-batch-route"), expected_vulns: 1 },
56248
+ { id: "php-complex", dir: join16(ROOT6, "php-complex"), expected_vulns: 4 }
56177
56249
  ];
56178
56250
  function sourceFiles(root) {
56179
56251
  const out = [];
@@ -56185,7 +56257,7 @@ function sourceFiles(root) {
56185
56257
  return;
56186
56258
  }
56187
56259
  for (const e of entries) {
56188
- const p = join15(dir, e);
56260
+ const p = join16(dir, e);
56189
56261
  let st;
56190
56262
  try {
56191
56263
  st = statSync3(p);
@@ -57788,9 +57860,13 @@ ${statusLine}` : statusLine },
57788
57860
  } catch {
57789
57861
  return { content: [{ type: "text", text: JSON.stringify({ error: "invalid JSON" }) }] };
57790
57862
  }
57863
+ const fmt = format ?? "markdown";
57791
57864
  const opts = { title, scope, version: VERSION };
57792
- const out = (format ?? "markdown") === "json" ? reportJson(list, opts) : reportMarkdown(list, opts);
57793
- return { content: [{ type: "text", text: out }] };
57865
+ const out = fmt === "json" ? reportJson(list, opts) : reportMarkdown(list, opts);
57866
+ const saved = saveReport(out, { title, scope, version: VERSION, format: fmt });
57867
+ return { content: [{ type: "text", text: `Report saved to: ${saved.path}
57868
+
57869
+ ${out}` }] };
57794
57870
  });
57795
57871
  server.registerTool("run_benchmark", {
57796
57872
  title: "Run the benchmark suite",
@@ -57832,12 +57908,12 @@ async function serve() {
57832
57908
  // src/index.ts
57833
57909
  init_cli();
57834
57910
  import { readFileSync as readFileSync23, existsSync as existsSync12 } from "node:fs";
57835
- import { join as join20 } from "node:path";
57911
+ import { join as join21 } from "node:path";
57836
57912
  import { fileURLToPath as fileURLToPath13 } from "node:url";
57837
- var ROOT11 = join20(fileURLToPath13(new URL(".", import.meta.url)), "..");
57913
+ var ROOT11 = join21(fileURLToPath13(new URL(".", import.meta.url)), "..");
57838
57914
  function readVersion() {
57839
57915
  try {
57840
- const p = join20(ROOT11, "package.json");
57916
+ const p = join21(ROOT11, "package.json");
57841
57917
  if (existsSync12(p))
57842
57918
  return JSON.parse(readFileSync23(p, "utf8")).version ?? "1.0.0";
57843
57919
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.21",
3
+ "version": "2.4.23",
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": {