blitzstrike 2.4.66 → 2.4.68

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 +328 -105
  3. package/package.json +1 -1
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
+ ## [2.4.68] — 2026-09-14
9
+
10
+ ### Added — Git-history secret mining + leaked-key validation
11
+
12
+ - New `scan_git_history` tool: mine a repo's HISTORY (not just HEAD) for secrets in
13
+ files that were DELETED — the "removed later != fixed" trap. Runs
14
+ `git log --diff-filter=D` + `git show <sha>^:<path>` (read-only) and feeds every
15
+ recovered pre-deletion blob through the hardcoded_secret detector, with the
16
+ deletion commit + message attached. Also a pickaxe mode (git log -S) to trace one
17
+ specific token through all commits.
18
+ - New `validate_leaked_key` tool: prove a recovered credential is still ACTIVE on a
19
+ live API with a READ-ONLY differential test — the same endpoint is fetched
20
+ without the key (expect 401/403) and with the key (expect 200); the contrast is
21
+ proof of auth bypass. Accepts a raw key or a blobUrl + varName to fetch/parse it.
22
+ - Secret severity now ESCALATES: when a secret-field literal also matches a known
23
+ format (e.g. a Stripe live key under a SECRET_KEY field), the finding takes the
24
+ format's more precise severity (critical) instead of the generic field tier.
25
+
26
+ ## [2.4.67] — 2026-09-14
27
+
28
+ ### Fixed — Whole-code interconnection audit (2 bugs)
29
+
30
+ - `submission_gate` is now FAIL-CLOSED: a question passes only when EXPLICITLY
31
+ asserted true (`=== true`, not `!== false`), so an empty/unset finding fails the
32
+ gate instead of passing 7/7 by default. One false = kill, and a missing field is
33
+ a false.
34
+ - Redaction now covers detection end-to-end: 6 secret formats the detector found
35
+ but the redactor did not strip (Slack webhook URL, Firebase URL, basic-auth URL,
36
+ Sentry DSN, ngrok auth token, Discord bot token) now redact — the invariant
37
+ "every secret the detector finds is stripped from reports/logs" is restored.
38
+ - Added a whole-code interconnection audit (detectors ↔ CWE ↔ patterns ↔ chains ↔
39
+ variants ↔ synonyms ↔ severity ↔ secrets + edge cases) + regression tests for
40
+ both fixes.
41
+
8
42
  ## [2.4.66] — 2026-09-14
9
43
 
10
44
  ### Added — Per-engagement pattern recall (intelligence ledger)
package/dist/index.js CHANGED
@@ -7495,6 +7495,12 @@ var init_secrets = __esm(() => {
7495
7495
  [/\blin_api_[A-Za-z0-9]{40}\b/g, "[REDACTED]"],
7496
7496
  [/\b(?:NRAA|NRAK|NRBR)-[A-F0-9]{27}\b/g, "[REDACTED]"],
7497
7497
  [/\b\d{8,10}:[A-Za-z0-9_-]{35}\b/g, "[REDACTED]"],
7498
+ [/\b[12][A-Za-z0-9]{26}_[A-Za-z0-9]{32,}\b/g, "[REDACTED]"],
7499
+ [/\b[MN][A-Za-z\d]{23}\.[\w-]{6}\.[\w-]{27}\b/g, "[REDACTED]"],
7500
+ [/https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9\/]+/g, "[REDACTED]"],
7501
+ [/\bhttps?:\/\/[a-z0-9-]+\.firebaseio\.com\b/g, "[REDACTED]"],
7502
+ [/https:\/\/[a-f0-9]+@o[0-9]+\.ingest\.sentry\.io\/[0-9]+/g, "[REDACTED]"],
7503
+ [/https?:\/\/[^/\s:@]+:[^/\s:@]+@[^/\s]+/g, "[REDACTED]"],
7498
7504
  [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g, "[REDACTED]"],
7499
7505
  [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
7500
7506
  ];
@@ -16517,14 +16523,26 @@ function detectHardcodedSecret(code, file, out) {
16517
16523
  continue;
16518
16524
  const ln = lineNo2(code, m.index);
16519
16525
  flaggedLines.add(ln);
16526
+ let sev = "high";
16527
+ let cat = "Hardcoded secret (credential in source)";
16528
+ let detail = `Secret field '${field}' is assigned a hardcoded value in source — the credential is exposed to anyone with code access (CWE-798 Hard-coded Credentials). Rotate it and load from the environment instead.`;
16529
+ for (const fmt of SECRET_FORMATS) {
16530
+ fmt.re.lastIndex = 0;
16531
+ if (fmt.re.test(value)) {
16532
+ sev = fmt.severity;
16533
+ cat = `Hardcoded secret (${fmt.category}: ${fmt.name})`;
16534
+ detail = `${fmt.name} was found hardcoded in source — the credential is exposed to anyone with code access (CWE-798, severity ${fmt.severity}). Rotate it and load from the environment instead.`;
16535
+ break;
16536
+ }
16537
+ }
16520
16538
  out.push({
16521
16539
  file,
16522
16540
  line: ln,
16523
16541
  type: "hardcoded_secret",
16524
- category: "Hardcoded secret (credential in source)",
16525
- severity: "high",
16542
+ category: cat,
16543
+ severity: sev,
16526
16544
  evidence: (ls[ln - 1] ?? "").trim(),
16527
- detail: `Secret field '${field}' is assigned a hardcoded value in source — the credential is exposed to anyone with code access (CWE-798 Hard-coded Credentials). Rotate it and load from the environment instead.`
16545
+ detail
16528
16546
  });
16529
16547
  }
16530
16548
  for (const fmt of SECRET_FORMATS) {
@@ -16848,11 +16866,11 @@ $r = openai.chat.completions.create(['messages' => [['role' => 'user', 'content'
16848
16866
 
16849
16867
  // src/variants.ts
16850
16868
  import { readFileSync as readFileSync19 } from "node:fs";
16851
- import { join as join18, dirname as dirname4 } from "node:path";
16869
+ import { join as join19, dirname as dirname4 } from "node:path";
16852
16870
  import { fileURLToPath as fileURLToPath10 } from "node:url";
16853
16871
  function loadVariants() {
16854
16872
  try {
16855
- const doc = JSON.parse(readFileSync19(join18(__dirname4, "..", "intelligence", "variants.json"), "utf8"));
16873
+ const doc = JSON.parse(readFileSync19(join19(__dirname4, "..", "intelligence", "variants.json"), "utf8"));
16856
16874
  return doc.variants ?? [];
16857
16875
  } catch {
16858
16876
  return [];
@@ -16912,7 +16930,7 @@ var init_variants = __esm(() => {
16912
16930
 
16913
16931
  // src/sync.ts
16914
16932
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync25, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
16915
- import { join as join22 } from "node:path";
16933
+ import { join as join23 } from "node:path";
16916
16934
  import { homedir as homedir10 } from "node:os";
16917
16935
  import { fileURLToPath as fileURLToPath12 } from "node:url";
16918
16936
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -16929,7 +16947,7 @@ function countFiles(dir) {
16929
16947
  const walk = (d) => {
16930
16948
  for (const e of readdirSync7(d, { withFileTypes: true })) {
16931
16949
  if (e.isDirectory())
16932
- walk(join22(d, e.name));
16950
+ walk(join23(d, e.name));
16933
16951
  else
16934
16952
  n++;
16935
16953
  }
@@ -16943,8 +16961,8 @@ function countFiles(dir) {
16943
16961
  function syncData() {
16944
16962
  if (!dirExists(DATA_ROOT2))
16945
16963
  mkdirSync9(DATA_ROOT2, { recursive: true });
16946
- const payloadsPath = join22(DATA_ROOT2, "payloads");
16947
- const templatesPath = join22(DATA_ROOT2, "templates");
16964
+ const payloadsPath = join23(DATA_ROOT2, "payloads");
16965
+ const templatesPath = join23(DATA_ROOT2, "templates");
16948
16966
  const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
16949
16967
  const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
16950
16968
  if (hasPayloads && hasTemplates) {
@@ -16954,8 +16972,8 @@ function syncData() {
16954
16972
  data_root: DATA_ROOT2
16955
16973
  };
16956
16974
  }
16957
- const localPayloads = join22(ROOT7, "payloads");
16958
- const localTemplates = join22(ROOT7, "templates");
16975
+ const localPayloads = join23(ROOT7, "payloads");
16976
+ const localTemplates = join23(ROOT7, "templates");
16959
16977
  let payloadsSynced = false;
16960
16978
  let templatesSynced = false;
16961
16979
  if (!hasPayloads && dirExists(localPayloads)) {
@@ -16968,7 +16986,7 @@ function syncData() {
16968
16986
  }
16969
16987
  const gitOk = spawnSync4("git", ["--version"]).status === 0;
16970
16988
  if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
16971
- const tmp = join22(DATA_ROOT2, ".tmp-clone");
16989
+ const tmp = join23(DATA_ROOT2, ".tmp-clone");
16972
16990
  if (dirExists(tmp))
16973
16991
  rmSync4(tmp, { recursive: true, force: true });
16974
16992
  const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
@@ -16976,11 +16994,11 @@ function syncData() {
16976
16994
  const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
16977
16995
  if (sparse.status === 0) {
16978
16996
  if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
16979
- copyDir(join22(tmp, "payloads"), payloadsPath);
16997
+ copyDir(join23(tmp, "payloads"), payloadsPath);
16980
16998
  payloadsSynced = true;
16981
16999
  }
16982
17000
  if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
16983
- copyDir(join22(tmp, "templates"), templatesPath);
17001
+ copyDir(join23(tmp, "templates"), templatesPath);
16984
17002
  templatesSynced = true;
16985
17003
  }
16986
17004
  }
@@ -17002,8 +17020,8 @@ function copyDir(src, dst) {
17002
17020
  function manualCopy(src, dst) {
17003
17021
  mkdirSync9(dst, { recursive: true });
17004
17022
  for (const e of readdirSync7(src, { withFileTypes: true })) {
17005
- const s = join22(src, e.name);
17006
- const d = join22(dst, e.name);
17023
+ const s = join23(src, e.name);
17024
+ const d = join23(dst, e.name);
17007
17025
  if (e.isDirectory())
17008
17026
  manualCopy(s, d);
17009
17027
  else
@@ -17012,8 +17030,8 @@ function manualCopy(src, dst) {
17012
17030
  }
17013
17031
  var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
17014
17032
  var init_sync = __esm(() => {
17015
- ROOT7 = join22(fileURLToPath12(new URL(".", import.meta.url)), "..");
17016
- DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join22(homedir10(), ".blitzstrike", "data");
17033
+ ROOT7 = join23(fileURLToPath12(new URL(".", import.meta.url)), "..");
17034
+ DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join23(homedir10(), ".blitzstrike", "data");
17017
17035
  });
17018
17036
 
17019
17037
  // src/update.ts
@@ -17058,15 +17076,15 @@ var init_update = __esm(() => {
17058
17076
  });
17059
17077
 
17060
17078
  // src/release.ts
17061
- import { readFileSync as readFileSync27, existsSync as existsSync12 } from "node:fs";
17062
- import { join as join23 } from "node:path";
17079
+ import { readFileSync as readFileSync27, existsSync as existsSync13 } from "node:fs";
17080
+ import { join as join24 } from "node:path";
17063
17081
  import { fileURLToPath as fileURLToPath13 } from "node:url";
17064
17082
  import { execSync as execSync2 } from "node:child_process";
17065
17083
  function validateRelease() {
17066
17084
  const checks = [];
17067
17085
  checks.push({ name: "package.json version", ok: true, detail: VERSION });
17068
17086
  try {
17069
- const changelog = readFileSync27(join23(ROOT8, "CHANGELOG.md"), "utf8");
17087
+ const changelog = readFileSync27(join24(ROOT8, "CHANGELOG.md"), "utf8");
17070
17088
  const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
17071
17089
  const changelogVersion = m?.[1] ?? null;
17072
17090
  checks.push({
@@ -17098,22 +17116,22 @@ function validateRelease() {
17098
17116
  return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
17099
17117
  }
17100
17118
  function lockfilePresent() {
17101
- return existsSync12(join23(ROOT8, "package-lock.json")) || existsSync12(join23(ROOT8, "bun.lock")) || existsSync12(join23(ROOT8, "bun.lockb"));
17119
+ return existsSync13(join24(ROOT8, "package-lock.json")) || existsSync13(join24(ROOT8, "bun.lock")) || existsSync13(join24(ROOT8, "bun.lockb"));
17102
17120
  }
17103
17121
  var ROOT8;
17104
17122
  var init_release = __esm(() => {
17105
17123
  init_version();
17106
- ROOT8 = join23(fileURLToPath13(new URL(".", import.meta.url)), "..");
17124
+ ROOT8 = join24(fileURLToPath13(new URL(".", import.meta.url)), "..");
17107
17125
  });
17108
17126
 
17109
17127
  // src/dependency.ts
17110
17128
  import { readFileSync as readFileSync28 } from "node:fs";
17111
- import { join as join24 } from "node:path";
17129
+ import { join as join25 } from "node:path";
17112
17130
  import { fileURLToPath as fileURLToPath14 } from "node:url";
17113
17131
  function checkDependencies() {
17114
17132
  let pkg = {};
17115
17133
  try {
17116
- pkg = JSON.parse(readFileSync28(join24(ROOT9, "package.json"), "utf8"));
17134
+ pkg = JSON.parse(readFileSync28(join25(ROOT9, "package.json"), "utf8"));
17117
17135
  } catch {
17118
17136
  return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
17119
17137
  }
@@ -17136,12 +17154,12 @@ function checkDependencies() {
17136
17154
  }
17137
17155
  var ROOT9;
17138
17156
  var init_dependency = __esm(() => {
17139
- ROOT9 = join24(fileURLToPath14(new URL(".", import.meta.url)), "..");
17157
+ ROOT9 = join25(fileURLToPath14(new URL(".", import.meta.url)), "..");
17140
17158
  });
17141
17159
 
17142
17160
  // src/cli.ts
17143
- import { readFileSync as readFileSync29, existsSync as existsSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
17144
- import { join as join25, dirname as dirname5 } from "node:path";
17161
+ import { readFileSync as readFileSync29, existsSync as existsSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
17162
+ import { join as join26, dirname as dirname5 } from "node:path";
17145
17163
  import { homedir as homedir11 } from "node:os";
17146
17164
  import { fileURLToPath as fileURLToPath15 } from "node:url";
17147
17165
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -17186,7 +17204,7 @@ async function runDoctor() {
17186
17204
  detail: h1 ? `X-HackerOne-Research: ${h1}` : "H1_USERNAME not set",
17187
17205
  fix: h1 ? undefined : "export H1_USERNAME=<your-hackerone-username> (adds X-HackerOne-Research to outbound requests)"
17188
17206
  });
17189
- const dataOk = existsSync13(join25(ROOT10, "chains.json")) && existsSync13(join25(ROOT10, "tools-catalog.json"));
17207
+ const dataOk = existsSync14(join26(ROOT10, "chains.json")) && existsSync14(join26(ROOT10, "tools-catalog.json"));
17190
17208
  issues.push({
17191
17209
  name: "Data layers (chains + tools-catalog)",
17192
17210
  status: dataOk ? "ok" : "fail",
@@ -17237,15 +17255,15 @@ BlitzStrike — ${verdict}`);
17237
17255
  function resolveCommand() {
17238
17256
  if (which("blitzstrike"))
17239
17257
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
17240
- const src = join25(ROOT10, "src", "index.ts");
17241
- if (which("bun") && existsSync13(src))
17258
+ const src = join26(ROOT10, "src", "index.ts");
17259
+ if (which("bun") && existsSync14(src))
17242
17260
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
17243
17261
  if (which("npx"))
17244
17262
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
17245
17263
  if (which("bunx"))
17246
17264
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
17247
- const dist = join25(ROOT10, "dist", "index.js");
17248
- if (existsSync13(dist))
17265
+ const dist = join26(ROOT10, "dist", "index.js");
17266
+ if (existsSync14(dist))
17249
17267
  return { command: "bun", args: [dist, "serve", "--mcp"] };
17250
17268
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
17251
17269
  }
@@ -17259,7 +17277,7 @@ function readJson(p) {
17259
17277
  function jsonMcpServersWrite(p) {
17260
17278
  return (command, args) => {
17261
17279
  const dir = dirname5(p);
17262
- if (!existsSync13(dir))
17280
+ if (!existsSync14(dir))
17263
17281
  mkdirSync10(dir, { recursive: true });
17264
17282
  const existing = readJson(p) ?? {};
17265
17283
  existing.mcpServers = {
@@ -17272,7 +17290,7 @@ function jsonMcpServersWrite(p) {
17272
17290
  function opencodeWrite(p) {
17273
17291
  return (command, args) => {
17274
17292
  const dir = dirname5(p);
17275
- if (!existsSync13(dir))
17293
+ if (!existsSync14(dir))
17276
17294
  mkdirSync10(dir, { recursive: true });
17277
17295
  const existing = readJson(p) ?? {};
17278
17296
  existing.mcp = {
@@ -17284,16 +17302,16 @@ function opencodeWrite(p) {
17284
17302
  };
17285
17303
  }
17286
17304
  function copyOpenCodeAgents() {
17287
- const srcDir = join25(ROOT10, "opencode-agents");
17288
- const dstDir = join25(homedir11(), ".config", "opencode", "agents");
17289
- if (!existsSync13(srcDir))
17305
+ const srcDir = join26(ROOT10, "opencode-agents");
17306
+ const dstDir = join26(homedir11(), ".config", "opencode", "agents");
17307
+ if (!existsSync14(srcDir))
17290
17308
  return;
17291
- if (!existsSync13(dstDir))
17309
+ if (!existsSync14(dstDir))
17292
17310
  mkdirSync10(dstDir, { recursive: true });
17293
17311
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
17294
- const src = join25(srcDir, f);
17295
- const dst = join25(dstDir, f);
17296
- if (existsSync13(src)) {
17312
+ const src = join26(srcDir, f);
17313
+ const dst = join26(dstDir, f);
17314
+ if (existsSync14(src)) {
17297
17315
  writeFileSync9(dst, readFileSync29(src, "utf8"));
17298
17316
  }
17299
17317
  }
@@ -17301,9 +17319,9 @@ function copyOpenCodeAgents() {
17301
17319
  function codexWrite(p) {
17302
17320
  return (command, args) => {
17303
17321
  const dir = dirname5(p);
17304
- if (!existsSync13(dir))
17322
+ if (!existsSync14(dir))
17305
17323
  mkdirSync10(dir, { recursive: true });
17306
- let existing = existsSync13(p) ? readFileSync29(p, "utf8") : "";
17324
+ let existing = existsSync14(p) ? readFileSync29(p, "utf8") : "";
17307
17325
  if (!existing.trimEnd().endsWith(`
17308
17326
  `))
17309
17327
  existing += `
@@ -17320,9 +17338,9 @@ args = ${JSON.stringify(args)}
17320
17338
  function hermesWrite(p) {
17321
17339
  return (command, args) => {
17322
17340
  const dir = dirname5(p);
17323
- if (!existsSync13(dir))
17341
+ if (!existsSync14(dir))
17324
17342
  mkdirSync10(dir, { recursive: true });
17325
- let existing = existsSync13(p) ? readFileSync29(p, "utf8") : "";
17343
+ let existing = existsSync14(p) ? readFileSync29(p, "utf8") : "";
17326
17344
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
17327
17345
  if (!existing.trimEnd().endsWith(`
17328
17346
  `))
@@ -17349,74 +17367,74 @@ mcp_servers:
17349
17367
  function detectAgents() {
17350
17368
  const home = homedir11();
17351
17369
  const agents = [];
17352
- const claudeJson = join25(home, ".claude.json");
17370
+ const claudeJson = join26(home, ".claude.json");
17353
17371
  agents.push({
17354
17372
  name: "Claude Code",
17355
17373
  detectPath: claudeJson,
17356
- installed: () => existsSync13(claudeJson) || which("claude"),
17374
+ installed: () => existsSync14(claudeJson) || which("claude"),
17357
17375
  write: jsonMcpServersWrite(claudeJson)
17358
17376
  });
17359
- const claudeDesktop = join25(home, ".config", "Claude", "claude_desktop_config.json");
17377
+ const claudeDesktop = join26(home, ".config", "Claude", "claude_desktop_config.json");
17360
17378
  agents.push({
17361
17379
  name: "Claude Desktop",
17362
17380
  detectPath: claudeDesktop,
17363
- installed: () => existsSync13(claudeDesktop),
17381
+ installed: () => existsSync14(claudeDesktop),
17364
17382
  write: jsonMcpServersWrite(claudeDesktop)
17365
17383
  });
17366
- const cursor = join25(home, ".cursor", "mcp.json");
17384
+ const cursor = join26(home, ".cursor", "mcp.json");
17367
17385
  agents.push({
17368
17386
  name: "Cursor",
17369
17387
  detectPath: cursor,
17370
- installed: () => existsSync13(join25(home, ".cursor")) || which("cursor"),
17388
+ installed: () => existsSync14(join26(home, ".cursor")) || which("cursor"),
17371
17389
  write: jsonMcpServersWrite(cursor)
17372
17390
  });
17373
- const opencode = join25(home, ".config", "opencode", "opencode.jsonc");
17391
+ const opencode = join26(home, ".config", "opencode", "opencode.jsonc");
17374
17392
  agents.push({
17375
17393
  name: "OpenCode",
17376
17394
  detectPath: opencode,
17377
- installed: () => existsSync13(join25(home, ".config", "opencode")) || which("opencode"),
17395
+ installed: () => existsSync14(join26(home, ".config", "opencode")) || which("opencode"),
17378
17396
  write: opencodeWrite(opencode)
17379
17397
  });
17380
- const codex = join25(home, ".codex", "config.toml");
17398
+ const codex = join26(home, ".codex", "config.toml");
17381
17399
  agents.push({
17382
17400
  name: "Codex",
17383
17401
  detectPath: codex,
17384
- installed: () => existsSync13(join25(home, ".codex")) || which("codex"),
17402
+ installed: () => existsSync14(join26(home, ".codex")) || which("codex"),
17385
17403
  write: codexWrite(codex)
17386
17404
  });
17387
- const hermes = join25(home, ".hermes", "config.yaml");
17405
+ const hermes = join26(home, ".hermes", "config.yaml");
17388
17406
  agents.push({
17389
17407
  name: "Hermes",
17390
17408
  detectPath: hermes,
17391
- installed: () => existsSync13(join25(home, ".hermes")) || which("hermes"),
17409
+ installed: () => existsSync14(join26(home, ".hermes")) || which("hermes"),
17392
17410
  write: hermesWrite(hermes)
17393
17411
  });
17394
- const gemini = join25(home, ".gemini", "settings.json");
17412
+ const gemini = join26(home, ".gemini", "settings.json");
17395
17413
  agents.push({
17396
17414
  name: "Gemini CLI",
17397
17415
  detectPath: gemini,
17398
- installed: () => existsSync13(join25(home, ".gemini")) || which("gemini"),
17416
+ installed: () => existsSync14(join26(home, ".gemini")) || which("gemini"),
17399
17417
  write: jsonMcpServersWrite(gemini)
17400
17418
  });
17401
- const windsurf = join25(home, ".codeium", "windsurf", "mcp_config.json");
17419
+ const windsurf = join26(home, ".codeium", "windsurf", "mcp_config.json");
17402
17420
  agents.push({
17403
17421
  name: "Windsurf",
17404
17422
  detectPath: windsurf,
17405
- installed: () => existsSync13(join25(home, ".codeium")) || which("windsurf"),
17423
+ installed: () => existsSync14(join26(home, ".codeium")) || which("windsurf"),
17406
17424
  write: jsonMcpServersWrite(windsurf)
17407
17425
  });
17408
- const copilot = join25(home, ".copilot", "mcp.json");
17426
+ const copilot = join26(home, ".copilot", "mcp.json");
17409
17427
  agents.push({
17410
17428
  name: "Copilot",
17411
17429
  detectPath: copilot,
17412
- installed: () => existsSync13(join25(home, ".copilot")),
17430
+ installed: () => existsSync14(join26(home, ".copilot")),
17413
17431
  write: jsonMcpServersWrite(copilot)
17414
17432
  });
17415
- const cline = join25(home, ".cline", "mcp_settings.json");
17433
+ const cline = join26(home, ".cline", "mcp_settings.json");
17416
17434
  agents.push({
17417
17435
  name: "Cline",
17418
17436
  detectPath: cline,
17419
- installed: () => existsSync13(join25(home, ".cline")),
17437
+ installed: () => existsSync14(join26(home, ".cline")),
17420
17438
  write: jsonMcpServersWrite(cline)
17421
17439
  });
17422
17440
  return agents;
@@ -17534,7 +17552,7 @@ var init_cli = __esm(() => {
17534
17552
  init_release();
17535
17553
  init_dependency();
17536
17554
  init_mcp_status();
17537
- ROOT10 = join25(fileURLToPath15(new URL(".", import.meta.url)), "..");
17555
+ ROOT10 = join26(fileURLToPath15(new URL(".", import.meta.url)), "..");
17538
17556
  });
17539
17557
 
17540
17558
  // node_modules/zod/v3/helpers/util.js
@@ -57818,37 +57836,37 @@ function submissionGate(input) {
57818
57836
  const questions = [
57819
57837
  {
57820
57838
  q: "Reproducible — a copy-paste-ready request or step list triggers it?",
57821
- pass: input.reproducible !== false,
57839
+ pass: input.reproducible === true,
57822
57840
  reason: "Without an exact reproduction the finding is a hypothesis, not a result."
57823
57841
  },
57824
57842
  {
57825
57843
  q: "In scope — the vulnerable asset is in the engagement scope?",
57826
- pass: input.in_scope !== false,
57844
+ pass: input.in_scope === true,
57827
57845
  reason: "Out-of-scope assets must not be submitted."
57828
57846
  },
57829
57847
  {
57830
57848
  q: "Real impact — maps to an accepted-impact class, not informational?",
57831
- pass: input.real_impact !== false,
57849
+ pass: input.real_impact === true,
57832
57850
  reason: "Informational-only findings dilute the validity ratio."
57833
57851
  },
57834
57852
  {
57835
57853
  q: "No privileged assumption — works without unrealistic attacker prerequisites?",
57836
- pass: input.no_privileged_assumption !== false,
57854
+ pass: input.no_privileged_assumption === true,
57837
57855
  reason: "'Admin can do X' is a centralization risk, not a vulnerability."
57838
57856
  },
57839
57857
  {
57840
57858
  q: "Verified live — confirmed with a marker + negative control, not just a static hint?",
57841
- pass: input.verified_live !== false,
57859
+ pass: input.verified_live === true,
57842
57860
  reason: "A static hit is a hypothesis until verified live (strike_verify / browser_validate)."
57843
57861
  },
57844
57862
  {
57845
57863
  q: "Evidence attached AND redacted — secrets/PII stripped, integrity-tagged?",
57846
- pass: input.evidence_redacted !== false,
57864
+ pass: input.evidence_redacted === true,
57847
57865
  reason: "Evidence must carry SHA-256-tagged, redacted records — no leaked secrets or victim PII."
57848
57866
  },
57849
57867
  {
57850
57868
  q: "Severity derived — CVSS/confidence computed, not guessed?",
57851
- pass: input.severity_derived !== false,
57869
+ pass: input.severity_derived === true,
57852
57870
  reason: "Severity is not confidence; derive it, don't assert it."
57853
57871
  }
57854
57872
  ];
@@ -57862,6 +57880,185 @@ function submissionGate(input) {
57862
57880
  };
57863
57881
  }
57864
57882
 
57883
+ // src/git-history.ts
57884
+ init_complex_bugs();
57885
+ import { execFileSync as execFileSync4 } from "node:child_process";
57886
+ import { existsSync as existsSync10 } from "node:fs";
57887
+ import { join as join17 } from "node:path";
57888
+ function parseDeletions(output) {
57889
+ const out = [];
57890
+ let commit = "";
57891
+ let msg = "";
57892
+ for (const line of output.split(`
57893
+ `)) {
57894
+ if (line.startsWith("__COMMIT__ ")) {
57895
+ const rest = line.slice("__COMMIT__ ".length);
57896
+ const sp = rest.indexOf(" ");
57897
+ commit = sp === -1 ? rest : rest.slice(0, sp);
57898
+ msg = sp === -1 ? "" : rest.slice(sp + 1);
57899
+ } else if (line.startsWith("D\t") || line.startsWith("D ")) {
57900
+ const path = line.slice(2).trim();
57901
+ if (path && commit)
57902
+ out.push({ commit, commit_msg: msg, path });
57903
+ }
57904
+ }
57905
+ return out;
57906
+ }
57907
+ function blobBeforeDeletion(root, commit, path) {
57908
+ return execFileSync4("git", ["-C", root, "show", `${commit}^:${path}`], {
57909
+ encoding: "utf8",
57910
+ maxBuffer: 10 * 1024 * 1024
57911
+ });
57912
+ }
57913
+ function scanGitHistory(root, opts) {
57914
+ const limit = opts?.limit ?? 30;
57915
+ const findings = [];
57916
+ if (!existsSync10(join17(root, ".git"))) {
57917
+ return { root, is_git_repo: false, total: 0, findings: [], error: "not a git repository (no .git dir)" };
57918
+ }
57919
+ if (opts?.secretPattern) {
57920
+ try {
57921
+ const pick = execFileSync4("git", ["-C", root, "log", "-S", opts.secretPattern, "--all", "--format=__COMMIT__ %H %s"], { encoding: "utf8", maxBuffer: 5 * 1024 * 1024 });
57922
+ const commits = pick.split(`
57923
+ `).filter(Boolean).map((l) => {
57924
+ const rest = l.startsWith("__COMMIT__ ") ? l.slice("__COMMIT__ ".length) : l;
57925
+ const sp = rest.indexOf(" ");
57926
+ return { hash: sp === -1 ? rest : rest.slice(0, sp), msg: sp === -1 ? "" : rest.slice(sp + 1) };
57927
+ });
57928
+ return {
57929
+ root,
57930
+ is_git_repo: true,
57931
+ mode: "pickaxe",
57932
+ pattern: opts.secretPattern,
57933
+ total: commits.length,
57934
+ commits,
57935
+ hint: "Commits where the pattern was added or removed — inspect each to recover the secret value."
57936
+ };
57937
+ } catch (e) {
57938
+ return { root, is_git_repo: true, mode: "pickaxe", pattern: opts.secretPattern, total: 0, error: String(e) };
57939
+ }
57940
+ }
57941
+ try {
57942
+ const log = execFileSync4("git", ["-C", root, "log", "--all", "--diff-filter=D", "--name-status", "--format=__COMMIT__ %H %s", "--no-renames"], { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
57943
+ const deletions = parseDeletions(log);
57944
+ const seen = new Set;
57945
+ let checked = 0;
57946
+ for (const d of deletions) {
57947
+ if (findings.length >= limit)
57948
+ break;
57949
+ const key = `${d.path}`;
57950
+ if (seen.has(key))
57951
+ continue;
57952
+ seen.add(key);
57953
+ try {
57954
+ const blob = blobBeforeDeletion(root, d.commit, d.path);
57955
+ checked++;
57956
+ const hits = detectComplexBugs(blob, d.path).filter((f) => f.type === "hardcoded_secret");
57957
+ for (const h of hits) {
57958
+ findings.push({
57959
+ path: d.path,
57960
+ commit: d.commit,
57961
+ commit_msg: d.commit_msg,
57962
+ severity: h.severity,
57963
+ category: h.category,
57964
+ evidence: h.evidence,
57965
+ detail: `A ${h.category} was found in a file DELETED from HEAD but still present in git history (commit ${d.commit}). Removed-later is not fixed — anyone with the repo history can recover it.`
57966
+ });
57967
+ }
57968
+ } catch {}
57969
+ }
57970
+ return {
57971
+ root,
57972
+ is_git_repo: true,
57973
+ mode: "deleted_files",
57974
+ deletion_commits: deletions.length,
57975
+ blobs_checked: checked,
57976
+ total: findings.length,
57977
+ findings,
57978
+ hint: "Secrets in deleted files remain in .git history. Rotate any recovered credential and rewrite/scrub history."
57979
+ };
57980
+ } catch (e) {
57981
+ return { root, is_git_repo: true, mode: "deleted_files", total: 0, findings: [], error: String(e) };
57982
+ }
57983
+ }
57984
+
57985
+ // src/key-validation.ts
57986
+ function parseValueFromBlob(text, varName) {
57987
+ const re = new RegExp(`${varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[=:]\\s*["']?([^"'\\n]+)`);
57988
+ const m = text.match(re);
57989
+ if (!m)
57990
+ throw new Error(`variable ${varName} not found in blob`);
57991
+ return m[1].trim();
57992
+ }
57993
+ async function httpGet3(url, headers) {
57994
+ try {
57995
+ const res = await fetch(url, { headers, redirect: "follow" });
57996
+ return { status: res.status, body: await res.text() };
57997
+ } catch (e) {
57998
+ return { status: null, body: String(e) };
57999
+ }
58000
+ }
58001
+ async function validateLeakedKey(input) {
58002
+ const endpoints = input.endpoints ?? ["/items"];
58003
+ const scheme = input.scheme ?? "Bearer {value}";
58004
+ const header = input.header ?? "Authorization";
58005
+ let key = input.key ?? "";
58006
+ if (!key && input.blobUrl) {
58007
+ const blobRes = await httpGet3(input.blobUrl, { "User-Agent": "Mozilla/5.0 (recon-agent)" });
58008
+ if (blobRes.status !== 200) {
58009
+ return { ok: false, error: `blob fetch failed (HTTP ${blobRes.status}) — leak gone, privatized, or wrong sha`, step: "blob_fetch" };
58010
+ }
58011
+ try {
58012
+ key = parseValueFromBlob(blobRes.body, input.varName ?? "SECRET_KEY");
58013
+ } catch (e) {
58014
+ return { ok: false, error: String(e), step: "parse" };
58015
+ }
58016
+ }
58017
+ if (!key) {
58018
+ return { ok: false, error: "no key provided and no blobUrl to recover one from", step: "key" };
58019
+ }
58020
+ const authHeader = { [header]: scheme.replace("{value}", key) };
58021
+ const results = [];
58022
+ let proven = false;
58023
+ for (const ep of endpoints) {
58024
+ const url = input.apiBase.replace(/\/$/, "") + (ep.startsWith("/") ? ep : "/" + ep);
58025
+ const r0 = await httpGet3(url, { "User-Agent": "Mozilla/5.0 (recon-agent)" });
58026
+ const r1 = await httpGet3(url, { "User-Agent": "Mozilla/5.0 (recon-agent)", ...authHeader });
58027
+ let verdict = "-";
58028
+ if (r0.status === null || r1.status === null)
58029
+ verdict = "ERROR";
58030
+ else if ((r0.status === 401 || r0.status === 403) && r1.status === 200) {
58031
+ verdict = "BYPASS";
58032
+ proven = true;
58033
+ }
58034
+ let records = null;
58035
+ if (r1.status === 200) {
58036
+ try {
58037
+ records = (JSON.parse(r1.body).data ?? []).length;
58038
+ } catch {
58039
+ records = null;
58040
+ }
58041
+ }
58042
+ results.push({
58043
+ endpoint: ep,
58044
+ no_auth: r0.status,
58045
+ with_key: r1.status,
58046
+ records,
58047
+ verdict,
58048
+ error: verdict === "ERROR" ? r0.status === null ? r0.body : r1.body : undefined
58049
+ });
58050
+ }
58051
+ return {
58052
+ ok: true,
58053
+ proven,
58054
+ key_len: key.length,
58055
+ header,
58056
+ scheme,
58057
+ results,
58058
+ verdict: proven ? "AUTH BYPASS PROVEN — leaked credential valid on live system (read-only)" : "not proven — rotated? different header scheme? check manually"
58059
+ };
58060
+ }
58061
+
57865
58062
  // src/severity.ts
57866
58063
  var VRT_MAP = {
57867
58064
  sql_injection: { vrt: "injection > sql_injection", priority: "P1", severity: "critical", rationale: "SQL injection reaches sensitive data or enables admin takeover." },
@@ -57919,12 +58116,12 @@ function listSeverityMap() {
57919
58116
 
57920
58117
  // src/datarefresh.ts
57921
58118
  import { readFileSync as readFileSync18 } from "node:fs";
57922
- import { join as join17, dirname as dirname3 } from "node:path";
58119
+ import { join as join18, dirname as dirname3 } from "node:path";
57923
58120
  import { fileURLToPath as fileURLToPath9 } from "node:url";
57924
58121
  var __dirname3 = dirname3(fileURLToPath9(import.meta.url));
57925
58122
  function loadJson2(rel) {
57926
58123
  try {
57927
- return JSON.parse(readFileSync18(join17(__dirname3, "..", rel), "utf8"));
58124
+ return JSON.parse(readFileSync18(join18(__dirname3, "..", rel), "utf8"));
57928
58125
  } catch {
57929
58126
  return null;
57930
58127
  }
@@ -57960,13 +58157,13 @@ init_variants();
57960
58157
 
57961
58158
  // src/benchmark.ts
57962
58159
  import { readFileSync as readFileSync20, readdirSync as readdirSync5, writeFileSync as writeFileSync6, rmSync as rmSync3, mkdirSync as mkdirSync7 } from "node:fs";
57963
- import { join as join19 } from "node:path";
58160
+ import { join as join20 } from "node:path";
57964
58161
  import { fileURLToPath as fileURLToPath11 } from "node:url";
57965
58162
  import { tmpdir as tmpdir3, homedir as homedir8 } from "node:os";
57966
58163
  init_complex_bugs();
57967
- var ROOT5 = join19(fileURLToPath11(new URL(".", import.meta.url)), "..");
57968
- var CORPUS = join19(ROOT5, "bench", "corpus");
57969
- var CAPTURED_CORPUS_DIR = process.env.BLITZSTRIKE_CORPUS_DIR ?? join19(homedir8(), ".blitzstrike", "corpus");
58164
+ var ROOT5 = join20(fileURLToPath11(new URL(".", import.meta.url)), "..");
58165
+ var CORPUS = join20(ROOT5, "bench", "corpus");
58166
+ var CAPTURED_CORPUS_DIR = process.env.BLITZSTRIKE_CORPUS_DIR ?? join20(homedir8(), ".blitzstrike", "corpus");
57970
58167
  function parseCorpusFile(path) {
57971
58168
  let text;
57972
58169
  try {
@@ -58026,7 +58223,7 @@ function loadDir(dir) {
58026
58223
  for (const f of readdirSync5(dir)) {
58027
58224
  if (!f.endsWith(".md"))
58028
58225
  continue;
58029
- const e = parseCorpusFile(join19(dir, f));
58226
+ const e = parseCorpusFile(join20(dir, f));
58030
58227
  if (e)
58031
58228
  entries.push(e);
58032
58229
  }
@@ -58053,7 +58250,7 @@ function captureFalsePositive(input) {
58053
58250
  input.code,
58054
58251
  "```"
58055
58252
  ].filter((l) => l !== null);
58056
- const path = join19(CAPTURED_CORPUS_DIR, `${id}.md`);
58253
+ const path = join20(CAPTURED_CORPUS_DIR, `${id}.md`);
58057
58254
  writeFileSync6(path, lines.join(`
58058
58255
  `) + `
58059
58256
  `, "utf8");
@@ -58075,7 +58272,7 @@ function detect(entry) {
58075
58272
  }
58076
58273
  const lang = entry.language;
58077
58274
  if (lang === "php") {
58078
- const tmp = join19(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
58275
+ const tmp = join20(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
58079
58276
  writeFileSync6(tmp, entry.code);
58080
58277
  const r = analyzeDataFlow2(tmp);
58081
58278
  rmSync3(tmp, { force: true });
@@ -58144,21 +58341,21 @@ function runBenchmark() {
58144
58341
  }
58145
58342
 
58146
58343
  // src/obligations.ts
58147
- import { readFileSync as readFileSync21, appendFileSync as appendFileSync4, mkdirSync as mkdirSync8, existsSync as existsSync10, writeFileSync as writeFileSync7 } from "node:fs";
58148
- import { join as join20 } from "node:path";
58344
+ import { readFileSync as readFileSync21, appendFileSync as appendFileSync4, mkdirSync as mkdirSync8, existsSync as existsSync11, writeFileSync as writeFileSync7 } from "node:fs";
58345
+ import { join as join21 } from "node:path";
58149
58346
  import { createHash as createHash6 } from "node:crypto";
58150
58347
  import { homedir as homedir9 } from "node:os";
58151
- var HOME_DIR4 = process.env.BLITZSTRIKE_HOME ?? join20(homedir9(), ".blitzstrike");
58152
- var OBLIGATION_PATH = join20(HOME_DIR4, "obligations.jsonl");
58348
+ var HOME_DIR4 = process.env.BLITZSTRIKE_HOME ?? join21(homedir9(), ".blitzstrike");
58349
+ var OBLIGATION_PATH = join21(HOME_DIR4, "obligations.jsonl");
58153
58350
  function ensureDir3() {
58154
- if (!existsSync10(HOME_DIR4))
58351
+ if (!existsSync11(HOME_DIR4))
58155
58352
  mkdirSync8(HOME_DIR4, { recursive: true });
58156
58353
  }
58157
58354
  function shortId2(claim) {
58158
58355
  return `obl-${createHash6("sha256").update(`${claim}\x00${Date.now()}`).digest("hex").slice(0, 12)}`;
58159
58356
  }
58160
58357
  function loadObligations() {
58161
- if (!existsSync10(OBLIGATION_PATH))
58358
+ if (!existsSync11(OBLIGATION_PATH))
58162
58359
  return [];
58163
58360
  try {
58164
58361
  const out = [];
@@ -59161,17 +59358,17 @@ function analyzeJwt(token, opts = {}) {
59161
59358
  }
59162
59359
 
59163
59360
  // src/enterprise-benchmark.ts
59164
- import { existsSync as existsSync11, readFileSync as readFileSync24, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
59165
- import { join as join21, resolve } from "node:path";
59361
+ import { existsSync as existsSync12, readFileSync as readFileSync24, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
59362
+ import { join as join22, resolve } from "node:path";
59166
59363
  init_complex_bugs();
59167
59364
  var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
59168
59365
  var ENTERPRISE_FIXTURES = [
59169
- { id: "php-laravel", dir: join21(ROOT6, "php-laravel"), expected_vulns: 4 },
59170
- { id: "js-express", dir: join21(ROOT6, "js-express"), expected_vulns: 2 },
59171
- { id: "py-django", dir: join21(ROOT6, "py-django"), expected_vulns: 2 },
59172
- { id: "java-spring", dir: join21(ROOT6, "java-spring"), expected_vulns: 2 },
59173
- { id: "wp-batch-route", dir: join21(ROOT6, "wp-batch-route"), expected_vulns: 3 },
59174
- { id: "php-complex", dir: join21(ROOT6, "php-complex"), expected_vulns: 4 }
59366
+ { id: "php-laravel", dir: join22(ROOT6, "php-laravel"), expected_vulns: 4 },
59367
+ { id: "js-express", dir: join22(ROOT6, "js-express"), expected_vulns: 2 },
59368
+ { id: "py-django", dir: join22(ROOT6, "py-django"), expected_vulns: 2 },
59369
+ { id: "java-spring", dir: join22(ROOT6, "java-spring"), expected_vulns: 2 },
59370
+ { id: "wp-batch-route", dir: join22(ROOT6, "wp-batch-route"), expected_vulns: 3 },
59371
+ { id: "php-complex", dir: join22(ROOT6, "php-complex"), expected_vulns: 4 }
59175
59372
  ];
59176
59373
  function sourceFiles(root) {
59177
59374
  const out = [];
@@ -59183,7 +59380,7 @@ function sourceFiles(root) {
59183
59380
  return;
59184
59381
  }
59185
59382
  for (const e of entries) {
59186
- const p = join21(dir, e);
59383
+ const p = join22(dir, e);
59187
59384
  let st;
59188
59385
  try {
59189
59386
  st = statSync3(p);
@@ -59222,7 +59419,7 @@ function detectFixture(dir) {
59222
59419
  function runEnterpriseBenchmark() {
59223
59420
  const fixtures = [];
59224
59421
  for (const fx of ENTERPRISE_FIXTURES) {
59225
- if (!existsSync11(fx.dir)) {
59422
+ if (!existsSync12(fx.dir)) {
59226
59423
  fixtures.push({ id: fx.id, available: false, detected: 0, expected: fx.expected_vulns, recall: 0 });
59227
59424
  continue;
59228
59425
  }
@@ -59399,7 +59596,7 @@ function createServer() {
59399
59596
  ` + `- A hard block (Cloudflare/WAF/IP-block/captcha) is NOT the end: mark the finding blocked + note the required vantage (residential / authorized / correct-SNI / --resolve direct-to-origin), then CONTINUE to the next lead (origin bypass, non-prod cluster, mobile APK, subdomain).
59400
59597
  ` + `- Keep trying every lead AND every chain step until each is verified/executed or blocked, THEN generate_report. Never deliver a bare surface map, and never stop at 'found X' when X enables more.
59401
59598
 
59402
- ` + "TWO PATHS:\\n" + "- Source path on disk -> blitz_scan / taint_file / eagle_eye2 (static).\\n" + "- Live URL -> live_recon / active_scan / strike_verify (web-hunting).\\n\\n" + "DEEPEN A FINDING: attack_plan(target, tech, params) to get the prioritized vector plan (decide what to test next with data); list_chains (escalation), chain_links (compose), technique_lookup(cls), payload_lookup, bypass_lookup (blocked). Ground any detector hit empirically with pattern_lookup(id) — it returns the class's CWE, real-world signature, escalation chains, citable references, per-class bypass techniques, and the high-value assets (crown jewels) that class hits, so a raw hit becomes a class-aware finding. Resolve free-form terminology first with synonym_lookup(term) (IDOR/BOLA/SQLi/prompt-injection → canonical id). For an llm_injection hit, its pattern carries a validation discipline — the RUN-TWICE RULE (reproduce token-for-token across two fresh sessions), NON-GUESSABLE ANCHOR (must leak a real key/URL, not a plausible guess), and OOB PROOF (a verifiable callback log) — apply all three before reporting. BEFORE generating a report, run submission_gate(...) and only ship a finding that passes all 7 questions (reproducible, in-scope, real-impact, no-privileged-assumption, verified-live, evidence-redacted, severity-derived); one false = kill it. Calibrate severity with severity_calibrate(type, cvss) to get a P1–P5 tier + VRT-style category. Start each engagement with engagement_scaffold(target, scope) for the folder + scope.md structure, and check data_refresh() to confirm the data layers are consistent. Phase doctrine: read_skill(bs-orchestrate-engagement | bs-web-hunting | bs-source-audit | bs-verify-finding). Campaign plan: read_playbook(target-type). On any failure call retry_guidance(signal) / model_fallback(signal) before giving up.\n" + "KNOWLEDGE BASE (discover, don't guess): skillLookup(topic) / listSkills for the 40 attack-path + hunting skills (ap-*, hp-*, bs-*); list_attack_vectors / attack_vectors(cat) for the 34-category vector taxonomy; taxonomy(kind) for OWASP/CWE/ASVS/API-top-10; read_playbook(type) for the 17 campaign playbooks; list_manuals / read_tool_manual for 317 tool references; list_tools / run_catalog_tool for the 141-tool catalog. Every tool, skill, and data layer is reachable from these. CROSS-TARGET INTELLIGENCE: strike_resolve auto-records confirmed/false_positive verdicts into the empirical ledger; query it with intelligence(action=hit_rates/top_vectors/recall) so a new target on a known tech inherits historical hit-rates (attack_plan already blends them into success_probability). intelligence(action=recall, tech, target) is the per-engagement pattern recall — it returns which patterns historically CONFIRMED on that tech (prioritize them) versus which only ever produced false positives (avoid them), so a new engagement starts from the empirical playbook of past ones. LIVE PENTEST + TARGETING (black-box, WAF-safe): cache_gap(base_url, endpoint) fuzzes path-normalization + static-extension variants for cache deception/poisoning; blind_oracle(base_url, param, value) detects which SINK a param reaches (sql/ssti/xss) via benign arithmetic (id=1+0, test${7*7}) WITHOUT a malicious payload; param_precedence(base_url, param, A, B) maps duplicate-parameter precedence (first/last-wins) for WAF-bypass. SOURCE TARGETING: action_surface(code) maps every wp_ajax_* action to its vuln class + DERIVED priority (what to look for WHERE); security_state(sanitizers, sink) gives a wrong-context sanitization verdict; framework_knowledge(framework) returns auth/nonce primitives for 9 frameworks; variant_scan(root, signature) mass-sweeps an install base for ONE bug family; patch_analyze(old, new) REVERSES a security patch into the 0-day, and its mine_signature feeds variant_scan to sweep unpatched copies. All of these are DETECTION HINTS — verify each live (strike_verify / browser_validate) before it enters the report. NEWEST DETECTORS — understand AND try them: variant_scan also emits priv_esc (privilege escalation — a role/capability/user change fed by request input, traced cross-file via reverse call-graph, e.g. set_role($id, sanitize_key($_POST['um-role'])) or update_user_meta(...,'wp_capabilities', $_POST[...])) and flags uploadAllow=>array('all') unrestricted-upload configs as file_upload (the file-manager/connector shape). TO TRY a priv_esc hit: submit the role field with 'administrator' (or send wp_capabilities meta) and confirm the account actually gains admin. TO TRY an unrestricted-upload hit: POST a .php/.phtml and confirm it lands web-reachable + executes. variant_scan also emits hardcoded_secret (CWE-798) in three tiers: a secret field (api_key/db_password/access_token/aws_*) assigned a literal (HIGH), a known secret FORMAT (JWT eyJ…/AWS AKIA…/GitHub ghp_…/Slack xoxb-…/…) regardless of name (HIGH), or a high-entropy suspect token (MEDIUM — verify before reporting). Verify a frontend/JS secret live via drive_devtools (inspect the JS bundle / network response and confirm the key is actually exposed), and for a backend secret confirm it is reachable/valid. A static hit is a HYPOTHESIS until verified live — never report unverified.\n" + `PRECISION ANALYSIS — chrome-devtools MCP is MANDATORY, not optional:
59599
+ ` + "TWO PATHS:\\n" + "- Source path on disk -> blitz_scan / taint_file / eagle_eye2 (static).\\n" + "- Live URL -> live_recon / active_scan / strike_verify (web-hunting).\\n\\n" + "DEEPEN A FINDING: attack_plan(target, tech, params) to get the prioritized vector plan (decide what to test next with data); list_chains (escalation), chain_links (compose), technique_lookup(cls), payload_lookup, bypass_lookup (blocked). Ground any detector hit empirically with pattern_lookup(id) — it returns the class's CWE, real-world signature, escalation chains, citable references, per-class bypass techniques, and the high-value assets (crown jewels) that class hits, so a raw hit becomes a class-aware finding. Resolve free-form terminology first with synonym_lookup(term) (IDOR/BOLA/SQLi/prompt-injection → canonical id). For an llm_injection hit, its pattern carries a validation discipline — the RUN-TWICE RULE (reproduce token-for-token across two fresh sessions), NON-GUESSABLE ANCHOR (must leak a real key/URL, not a plausible guess), and OOB PROOF (a verifiable callback log) — apply all three before reporting. BEFORE generating a report, run submission_gate(...) and only ship a finding that passes all 7 questions (reproducible, in-scope, real-impact, no-privileged-assumption, verified-live, evidence-redacted, severity-derived); one false = kill it. Calibrate severity with severity_calibrate(type, cvss) to get a P1–P5 tier + VRT-style category. Start each engagement with engagement_scaffold(target, scope) for the folder + scope.md structure, and check data_refresh() to confirm the data layers are consistent. Phase doctrine: read_skill(bs-orchestrate-engagement | bs-web-hunting | bs-source-audit | bs-verify-finding). Campaign plan: read_playbook(target-type). On any failure call retry_guidance(signal) / model_fallback(signal) before giving up.\n" + "KNOWLEDGE BASE (discover, don't guess): skillLookup(topic) / listSkills for the 40 attack-path + hunting skills (ap-*, hp-*, bs-*); list_attack_vectors / attack_vectors(cat) for the 34-category vector taxonomy; taxonomy(kind) for OWASP/CWE/ASVS/API-top-10; read_playbook(type) for the 17 campaign playbooks; list_manuals / read_tool_manual for 317 tool references; list_tools / run_catalog_tool for the 141-tool catalog. Every tool, skill, and data layer is reachable from these. CROSS-TARGET INTELLIGENCE: strike_resolve auto-records confirmed/false_positive verdicts into the empirical ledger; query it with intelligence(action=hit_rates/top_vectors/recall) so a new target on a known tech inherits historical hit-rates (attack_plan already blends them into success_probability). intelligence(action=recall, tech, target) is the per-engagement pattern recall — it returns which patterns historically CONFIRMED on that tech (prioritize them) versus which only ever produced false positives (avoid them), so a new engagement starts from the empirical playbook of past ones. LIVE PENTEST + TARGETING (black-box, WAF-safe): cache_gap(base_url, endpoint) fuzzes path-normalization + static-extension variants for cache deception/poisoning; blind_oracle(base_url, param, value) detects which SINK a param reaches (sql/ssti/xss) via benign arithmetic (id=1+0, test${7*7}) WITHOUT a malicious payload; param_precedence(base_url, param, A, B) maps duplicate-parameter precedence (first/last-wins) for WAF-bypass. SOURCE TARGETING: action_surface(code) maps every wp_ajax_* action to its vuln class + DERIVED priority (what to look for WHERE); security_state(sanitizers, sink) gives a wrong-context sanitization verdict; framework_knowledge(framework) returns auth/nonce primitives for 9 frameworks; variant_scan(root, signature) mass-sweeps an install base for ONE bug family; patch_analyze(old, new) REVERSES a security patch into the 0-day, and its mine_signature feeds variant_scan to sweep unpatched copies. All of these are DETECTION HINTS — verify each live (strike_verify / browser_validate) before it enters the report. NEWEST DETECTORS — understand AND try them: variant_scan also emits priv_esc (privilege escalation — a role/capability/user change fed by request input, traced cross-file via reverse call-graph, e.g. set_role($id, sanitize_key($_POST['um-role'])) or update_user_meta(...,'wp_capabilities', $_POST[...])) and flags uploadAllow=>array('all') unrestricted-upload configs as file_upload (the file-manager/connector shape). TO TRY a priv_esc hit: submit the role field with 'administrator' (or send wp_capabilities meta) and confirm the account actually gains admin. TO TRY an unrestricted-upload hit: POST a .php/.phtml and confirm it lands web-reachable + executes. variant_scan also emits hardcoded_secret (CWE-798) in three tiers: a secret field (api_key/db_password/access_token/aws_*) assigned a literal (HIGH), a known secret FORMAT (JWT eyJ…/AWS AKIA…/GitHub ghp_…/Slack xoxb-…/…) regardless of name (HIGH), or a high-entropy suspect token (MEDIUM — verify before reporting). Verify a frontend/JS secret live via drive_devtools (inspect the JS bundle / network response and confirm the key is actually exposed), and for a backend secret confirm it is reachable/valid. A static hit is a HYPOTHESIS until verified live — never report unverified. For a repo, also scan_git_history(root) — secrets DELETED from HEAD still live in .git history (the removed-later-is-not-fixed trap), and it feeds every recovered pre-deletion blob through the same secret detector. Once you recover a credential, prove it is still ACTIVE with validate_leaked_key(apiBase, endpoints, key|blobUrl) — a READ-ONLY differential test (401-without-key vs 200-with-key = auth bypass PROVEN).\n" + `PRECISION ANALYSIS — chrome-devtools MCP is MANDATORY, not optional:
59403
59600
  ` + `- When a lead needs DOM/JS precision — DOM-XSS sink execution, AJAX endpoint + payload interception, JS runtime errors, redirect chains, SSO/token flow, or anything a static HTML glance cannot prove — call drive_devtools(target). It spawns chrome-devtools-mcp and drives a REAL headless browser (navigate -> list_network_requests -> list_console_messages -> evaluate_script), giving reproducible evidence instead of a guess.
59404
59601
  ` + `- Before calling it, check_mcp() to confirm the browser MCP is available; if not, blitzstrike install-tools or npm i -g chrome-devtools-mcp. Never substitute eyeballing minified JS or a static sink guess when a live browser can PROVE it. This is the precision + consistency layer.
59405
59602
  ` + "EXTERNAL MCP SERVERS (verified, connect them — NOT one-shot CLIs): chrome-devtools-mcp (29 tools: list_network_requests/get_network_request for raw HTTP interception, list_console_messages for JS errors + DOM-XSS, evaluate_script to run JS, take_screenshot) -> stdio chrome-devtools-mcp --headless --chromeArg=--no-sandbox --chromeArg=--disable-gpu (as root/container this is MANDATORY or the browser crashes; add --proxyServer http://127.0.0.1:8080 to route through Burp, --acceptInsecureCerts for bad TLS). burp-suite-mcp (proxy history + active scan + repeater) -> NOT npm: build the JAR (git clone https://github.com/PortSwigger/mcp-server.git && cd mcp-server && ./gradlew embedProxyJar), load it in Burp Extensions, then connect the client to http://127.0.0.1:9876 (SSE) or the stdio proxy java -jar mcp-proxy-all.jar --sse-url http://127.0.0.1:9876."
@@ -59843,6 +60040,32 @@ ${statusLine}` : statusLine },
59843
60040
  const r = synonymsFor(term);
59844
60041
  return { content: [{ type: "text", text: JSON.stringify({ ...r, pattern: patternLookup(r.canonical) }) }] };
59845
60042
  });
60043
+ server.registerTool("scan_git_history", {
60044
+ title: "Mine git history for deleted secrets",
60045
+ description: "DETECT: scan a git repo's HISTORY (not just HEAD) for secrets in files that were DELETED — the 'removed later != fixed' trap. Runs git log --diff-filter=D + git show <sha>^:<path> (read-only) and feeds every recovered pre-deletion blob through the hardcoded_secret detector. Pass secretPattern to instead pickaxe-search for one specific token through all commits.",
60046
+ inputSchema: {
60047
+ root: string2().describe("Path to the git repo root (must contain .git)."),
60048
+ secretPattern: string2().optional().describe("Optional: a specific secret token to pickaxe-search (git log -S) through history."),
60049
+ limit: number2().int().optional().describe("Max findings (default 30).")
60050
+ }
60051
+ }, async ({ root, secretPattern, limit }) => {
60052
+ return { content: [{ type: "text", text: JSON.stringify(scanGitHistory(root, { secretPattern, limit })) }] };
60053
+ });
60054
+ server.registerTool("validate_leaked_key", {
60055
+ title: "Prove a leaked key is still active (differential)",
60056
+ description: "VERIFY: prove a recovered credential is ACTIVE on a live API with a READ-ONLY differential test — the same endpoint is fetched without the key (expect 401/403) and with the key (expect 200). A 401-without / 200-with contrast is proof of auth bypass. Pass the raw key, or a blobUrl + varName to fetch/parse it from a leaked file first. No mutating request is ever sent.",
60057
+ inputSchema: {
60058
+ key: string2().optional().describe("Raw secret value to test (skip blob fetch)."),
60059
+ blobUrl: string2().optional().describe("Or: raw-blob URL (e.g. forge raw/commit/<sha>^/<path>) to fetch + parse the key from."),
60060
+ varName: string2().optional().describe("Variable name to parse from the blob (default SECRET_KEY)."),
60061
+ apiBase: string2().describe("Live API base URL."),
60062
+ endpoints: array(string2()).optional().describe("Read-only GET endpoints to probe (default ['/items'])."),
60063
+ scheme: string2().optional().describe("Header value template, e.g. 'Bearer {value}' (default)."),
60064
+ header: string2().optional().describe("Header name (default Authorization).")
60065
+ }
60066
+ }, async (input) => {
60067
+ return { content: [{ type: "text", text: JSON.stringify(await validateLeakedKey(input)) }] };
60068
+ });
59846
60069
  server.registerTool("fofa_search", {
59847
60070
  title: "FOFA asset search",
59848
60071
  description: "STRIKE: search the FOFA asset index (needs FOFA_EMAIL + FOFA_KEY).",
@@ -61187,14 +61410,14 @@ async function serve() {
61187
61410
 
61188
61411
  // src/index.ts
61189
61412
  init_cli();
61190
- import { readFileSync as readFileSync30, existsSync as existsSync14 } from "node:fs";
61191
- import { join as join26 } from "node:path";
61413
+ import { readFileSync as readFileSync30, existsSync as existsSync15 } from "node:fs";
61414
+ import { join as join27 } from "node:path";
61192
61415
  import { fileURLToPath as fileURLToPath16 } from "node:url";
61193
- var ROOT11 = join26(fileURLToPath16(new URL(".", import.meta.url)), "..");
61416
+ var ROOT11 = join27(fileURLToPath16(new URL(".", import.meta.url)), "..");
61194
61417
  function readVersion() {
61195
61418
  try {
61196
- const p = join26(ROOT11, "package.json");
61197
- if (existsSync14(p))
61419
+ const p = join27(ROOT11, "package.json");
61420
+ if (existsSync15(p))
61198
61421
  return JSON.parse(readFileSync30(p, "utf8")).version ?? "1.0.0";
61199
61422
  } catch {}
61200
61423
  return "1.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "2.4.66",
3
+ "version": "2.4.68",
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": {