blitzstrike 2.4.67 → 2.4.69

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/dist/index.js CHANGED
@@ -16263,6 +16263,8 @@ function detectComplexBugs(code, file) {
16263
16263
  detectGraphqlExposure(code, file, out);
16264
16264
  detectOauthMisconfig(code, file, out);
16265
16265
  detectGrpcReflection(code, file, out);
16266
+ detectDependencyConfusion(code, file, out);
16267
+ detectMlSupplyChain(code, file, out);
16266
16268
  out.push(...detectWrongSanitizer(code, file));
16267
16269
  out.push(...detectMissingAuthz(code, file));
16268
16270
  const seen = new Set;
@@ -16523,14 +16525,26 @@ function detectHardcodedSecret(code, file, out) {
16523
16525
  continue;
16524
16526
  const ln = lineNo2(code, m.index);
16525
16527
  flaggedLines.add(ln);
16528
+ let sev = "high";
16529
+ let cat = "Hardcoded secret (credential in source)";
16530
+ 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.`;
16531
+ for (const fmt of SECRET_FORMATS) {
16532
+ fmt.re.lastIndex = 0;
16533
+ if (fmt.re.test(value)) {
16534
+ sev = fmt.severity;
16535
+ cat = `Hardcoded secret (${fmt.category}: ${fmt.name})`;
16536
+ 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.`;
16537
+ break;
16538
+ }
16539
+ }
16526
16540
  out.push({
16527
16541
  file,
16528
16542
  line: ln,
16529
16543
  type: "hardcoded_secret",
16530
- category: "Hardcoded secret (credential in source)",
16531
- severity: "high",
16544
+ category: cat,
16545
+ severity: sev,
16532
16546
  evidence: (ls[ln - 1] ?? "").trim(),
16533
- 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.`
16547
+ detail
16534
16548
  });
16535
16549
  }
16536
16550
  for (const fmt of SECRET_FORMATS) {
@@ -16667,6 +16681,78 @@ function detectGrpcReflection(code, file, out) {
16667
16681
  });
16668
16682
  }
16669
16683
  }
16684
+ function detectDependencyConfusion(code, file, out) {
16685
+ const isManifest = /package\.json|requirements|pipfile|gemfile|go\.mod|pom\.xml|setup\.py|pyproject|\.npmrc|pip\.conf|dockerfile|\.ya?ml$/i.test(file);
16686
+ if (!isManifest)
16687
+ return;
16688
+ const ls = lines2(code);
16689
+ const idxRe = /--(?:extra-)?index-url\s+["']?(https?:\/\/[^"'<\s]+)["']?/gi;
16690
+ let m;
16691
+ while ((m = idxRe.exec(code)) !== null) {
16692
+ if (/pypi\.org|pythonhosted|npmjs\.org|rubygems\.org|golang\.org|crates\.io|maven\.apache\.org/i.test(m[1])) {
16693
+ const ln = lineNo2(code, m.index);
16694
+ out.push({
16695
+ file,
16696
+ line: ln,
16697
+ type: "dependency_confusion",
16698
+ category: "Dependency confusion — public registry in resolution path",
16699
+ severity: "high",
16700
+ evidence: (ls[ln - 1] ?? "").trim(),
16701
+ detail: `A public registry (${m[1]}) is the resolution source — an internal package name that exists or can be squatted on the public registry resolves there instead of the private one, pulling attacker-controlled code (CWE-427). Pin a private registry and namespace internal packages uniquely.`
16702
+ });
16703
+ }
16704
+ }
16705
+ const hasPrivateRegistry = /--(?:index-url|registry)\s+["']?(?!https?:\/\/pypi\.org|https?:\/\/registry\.npmjs\.org)/i.test(code) || /(?:registry|index[-_]url)\s*[:=]\s*["']https?:\/\/(?!.*(?:pypi\.org|registry\.npmjs\.org))/i.test(code);
16706
+ if (!hasPrivateRegistry) {
16707
+ const scopedRe = /["'](@[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+)["']/g;
16708
+ while ((m = scopedRe.exec(code)) !== null) {
16709
+ const ln = lineNo2(code, m.index);
16710
+ out.push({
16711
+ file,
16712
+ line: ln,
16713
+ type: "dependency_confusion",
16714
+ category: "Dependency confusion — scoped package, no private registry",
16715
+ severity: "medium",
16716
+ evidence: (ls[ln - 1] ?? "").trim(),
16717
+ detail: `The scoped package ${m[1]} is referenced but no private registry is pinned — if the @scope is not registered/reserved on the public registry, an attacker can publish ${m[1]} there and the build will pull malicious code (CWE-427). Verify the registry resolution and pin it.`
16718
+ });
16719
+ }
16720
+ }
16721
+ }
16722
+ function detectMlSupplyChain(code, file, out) {
16723
+ const ls = lines2(code);
16724
+ const clean = stripCommentsOnly(code);
16725
+ const sinks = [
16726
+ { re: /torch\.load\s*\(/g, name: "torch.load", note: "uses pickle; pass weights_only=True", skipIfSafe: /weights_only\s*=\s*True/ },
16727
+ { re: /\bjoblib\.load\s*\(/g, name: "joblib.load", note: "uses pickle (no safe flag)" },
16728
+ { re: /(?:tf\.)?keras\.models\.load_model\s*\(/g, name: "keras load_model", note: "deserializes the model graph (pickle/h5)" },
16729
+ { re: /\bnp\.load\s*\(/g, name: "np.load", note: "allow_pickle=True is RCE", requireFlag: /allow_pickle\s*=\s*True/i }
16730
+ ];
16731
+ for (const s of sinks) {
16732
+ s.re.lastIndex = 0;
16733
+ let m;
16734
+ while ((m = s.re.exec(clean)) !== null) {
16735
+ const ln = lineNo2(code, m.index);
16736
+ const l = ls[ln - 1] ?? "";
16737
+ if (s.skipIfSafe && s.skipIfSafe.test(l))
16738
+ continue;
16739
+ if (s.requireFlag && !s.requireFlag.test(l))
16740
+ continue;
16741
+ const flagIsSignal = s.requireFlag != null;
16742
+ if (!flagIsSignal && !SOURCE_TOKENS3.test(l) && !/https?:\/\//.test(l) && !/download|request|upload|user|input|temp|tmp/i.test(l))
16743
+ continue;
16744
+ out.push({
16745
+ file,
16746
+ line: ln,
16747
+ type: "ml_supply_chain",
16748
+ category: "ML supply chain — untrusted model load",
16749
+ severity: "high",
16750
+ evidence: l.trim(),
16751
+ detail: `\`${s.name}\` loads a model from an untrusted source — ${s.note}. A poisoned model file executes arbitrary code on load (CWE-502). Pin hashes, load from a trusted artifact store, and use the safe flag where available.`
16752
+ });
16753
+ }
16754
+ }
16755
+ }
16670
16756
  var SOURCE_TOKENS3, MAGIC_METHODS, SECRET_FIELD_RE, SECRET_PLACEHOLDER_RE, ENTROPY_EXCLUDE_VAR;
16671
16757
  var init_complex_bugs = __esm(() => {
16672
16758
  init_security_state();
@@ -16682,8 +16768,8 @@ var init_complex_bugs = __esm(() => {
16682
16768
  // src/verify-harness.ts
16683
16769
  function runVerificationHarness() {
16684
16770
  const cases = CASES.map((c) => {
16685
- const pos = detectComplexBugs(c.positive, "positive.php").filter((f) => f.type === c.detector);
16686
- const neg = detectComplexBugs(c.negative, "negative.php").filter((f) => f.type === c.detector);
16771
+ const pos = detectComplexBugs(c.positive, c.file ?? "positive.php").filter((f) => f.type === c.detector);
16772
+ const neg = detectComplexBugs(c.negative, c.file ?? "negative.php").filter((f) => f.type === c.detector);
16687
16773
  const positive_fired = pos.length > 0;
16688
16774
  const negative_fired = neg.length > 0;
16689
16775
  const tp = positive_fired;
@@ -16848,17 +16934,31 @@ $r = openai.chat.completions.create(['messages' => [['role' => 'user', 'content'
16848
16934
  detector: "grpc_reflection",
16849
16935
  positive: PHP(`$server->addService(enable_server_reflection());`),
16850
16936
  negative: PHP(`$server->addService($my_service);`)
16937
+ },
16938
+ {
16939
+ id: "dependency-confusion",
16940
+ detector: "dependency_confusion",
16941
+ file: "requirements.txt",
16942
+ positive: `--extra-index-url https://pypi.org/simple\\nnumpy==1.24.0\\n`,
16943
+ negative: `--index-url https://pypi.internal.company.com/simple\\nnumpy==1.24.0\\n`
16944
+ },
16945
+ {
16946
+ id: "ml-supply-chain",
16947
+ detector: "ml_supply_chain",
16948
+ file: "train.py",
16949
+ positive: `import torch\\nmodel = torch.load(downloaded_model)\\n`,
16950
+ negative: `import torch\\nmodel = torch.load("model.pt", weights_only=True)\\n`
16851
16951
  }
16852
16952
  ];
16853
16953
  });
16854
16954
 
16855
16955
  // src/variants.ts
16856
16956
  import { readFileSync as readFileSync19 } from "node:fs";
16857
- import { join as join18, dirname as dirname4 } from "node:path";
16957
+ import { join as join19, dirname as dirname4 } from "node:path";
16858
16958
  import { fileURLToPath as fileURLToPath10 } from "node:url";
16859
16959
  function loadVariants() {
16860
16960
  try {
16861
- const doc = JSON.parse(readFileSync19(join18(__dirname4, "..", "intelligence", "variants.json"), "utf8"));
16961
+ const doc = JSON.parse(readFileSync19(join19(__dirname4, "..", "intelligence", "variants.json"), "utf8"));
16862
16962
  return doc.variants ?? [];
16863
16963
  } catch {
16864
16964
  return [];
@@ -16867,7 +16967,7 @@ function loadVariants() {
16867
16967
  function scanVariants() {
16868
16968
  const variants = loadVariants().filter((v) => !CROSS_FILE_ONLY.has(v.class));
16869
16969
  const results = variants.map((v, i) => {
16870
- const findings = detectComplexBugs(v.code, `variant-${i}.php`).filter((f) => f.type === v.class);
16970
+ const findings = detectComplexBugs(v.code, v.file ?? `variant-${i}.php`).filter((f) => f.type === v.class);
16871
16971
  return { id: `variant-${i}`, class: v.class, fired: findings.length > 0, note: v.note };
16872
16972
  });
16873
16973
  const per_class = {};
@@ -16918,7 +17018,7 @@ var init_variants = __esm(() => {
16918
17018
 
16919
17019
  // src/sync.ts
16920
17020
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync25, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "node:fs";
16921
- import { join as join22 } from "node:path";
17021
+ import { join as join23 } from "node:path";
16922
17022
  import { homedir as homedir10 } from "node:os";
16923
17023
  import { fileURLToPath as fileURLToPath12 } from "node:url";
16924
17024
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -16935,7 +17035,7 @@ function countFiles(dir) {
16935
17035
  const walk = (d) => {
16936
17036
  for (const e of readdirSync7(d, { withFileTypes: true })) {
16937
17037
  if (e.isDirectory())
16938
- walk(join22(d, e.name));
17038
+ walk(join23(d, e.name));
16939
17039
  else
16940
17040
  n++;
16941
17041
  }
@@ -16949,8 +17049,8 @@ function countFiles(dir) {
16949
17049
  function syncData() {
16950
17050
  if (!dirExists(DATA_ROOT2))
16951
17051
  mkdirSync9(DATA_ROOT2, { recursive: true });
16952
- const payloadsPath = join22(DATA_ROOT2, "payloads");
16953
- const templatesPath = join22(DATA_ROOT2, "templates");
17052
+ const payloadsPath = join23(DATA_ROOT2, "payloads");
17053
+ const templatesPath = join23(DATA_ROOT2, "templates");
16954
17054
  const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
16955
17055
  const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
16956
17056
  if (hasPayloads && hasTemplates) {
@@ -16960,8 +17060,8 @@ function syncData() {
16960
17060
  data_root: DATA_ROOT2
16961
17061
  };
16962
17062
  }
16963
- const localPayloads = join22(ROOT7, "payloads");
16964
- const localTemplates = join22(ROOT7, "templates");
17063
+ const localPayloads = join23(ROOT7, "payloads");
17064
+ const localTemplates = join23(ROOT7, "templates");
16965
17065
  let payloadsSynced = false;
16966
17066
  let templatesSynced = false;
16967
17067
  if (!hasPayloads && dirExists(localPayloads)) {
@@ -16974,7 +17074,7 @@ function syncData() {
16974
17074
  }
16975
17075
  const gitOk = spawnSync4("git", ["--version"]).status === 0;
16976
17076
  if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
16977
- const tmp = join22(DATA_ROOT2, ".tmp-clone");
17077
+ const tmp = join23(DATA_ROOT2, ".tmp-clone");
16978
17078
  if (dirExists(tmp))
16979
17079
  rmSync4(tmp, { recursive: true, force: true });
16980
17080
  const r = spawnSync4("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
@@ -16982,11 +17082,11 @@ function syncData() {
16982
17082
  const sparse = spawnSync4("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
16983
17083
  if (sparse.status === 0) {
16984
17084
  if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
16985
- copyDir(join22(tmp, "payloads"), payloadsPath);
17085
+ copyDir(join23(tmp, "payloads"), payloadsPath);
16986
17086
  payloadsSynced = true;
16987
17087
  }
16988
17088
  if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
16989
- copyDir(join22(tmp, "templates"), templatesPath);
17089
+ copyDir(join23(tmp, "templates"), templatesPath);
16990
17090
  templatesSynced = true;
16991
17091
  }
16992
17092
  }
@@ -17008,8 +17108,8 @@ function copyDir(src, dst) {
17008
17108
  function manualCopy(src, dst) {
17009
17109
  mkdirSync9(dst, { recursive: true });
17010
17110
  for (const e of readdirSync7(src, { withFileTypes: true })) {
17011
- const s = join22(src, e.name);
17012
- const d = join22(dst, e.name);
17111
+ const s = join23(src, e.name);
17112
+ const d = join23(dst, e.name);
17013
17113
  if (e.isDirectory())
17014
17114
  manualCopy(s, d);
17015
17115
  else
@@ -17018,8 +17118,8 @@ function manualCopy(src, dst) {
17018
17118
  }
17019
17119
  var ROOT7, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
17020
17120
  var init_sync = __esm(() => {
17021
- ROOT7 = join22(fileURLToPath12(new URL(".", import.meta.url)), "..");
17022
- DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join22(homedir10(), ".blitzstrike", "data");
17121
+ ROOT7 = join23(fileURLToPath12(new URL(".", import.meta.url)), "..");
17122
+ DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join23(homedir10(), ".blitzstrike", "data");
17023
17123
  });
17024
17124
 
17025
17125
  // src/update.ts
@@ -17064,15 +17164,15 @@ var init_update = __esm(() => {
17064
17164
  });
17065
17165
 
17066
17166
  // src/release.ts
17067
- import { readFileSync as readFileSync27, existsSync as existsSync12 } from "node:fs";
17068
- import { join as join23 } from "node:path";
17167
+ import { readFileSync as readFileSync27, existsSync as existsSync13 } from "node:fs";
17168
+ import { join as join24 } from "node:path";
17069
17169
  import { fileURLToPath as fileURLToPath13 } from "node:url";
17070
17170
  import { execSync as execSync2 } from "node:child_process";
17071
17171
  function validateRelease() {
17072
17172
  const checks = [];
17073
17173
  checks.push({ name: "package.json version", ok: true, detail: VERSION });
17074
17174
  try {
17075
- const changelog = readFileSync27(join23(ROOT8, "CHANGELOG.md"), "utf8");
17175
+ const changelog = readFileSync27(join24(ROOT8, "CHANGELOG.md"), "utf8");
17076
17176
  const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
17077
17177
  const changelogVersion = m?.[1] ?? null;
17078
17178
  checks.push({
@@ -17104,22 +17204,22 @@ function validateRelease() {
17104
17204
  return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
17105
17205
  }
17106
17206
  function lockfilePresent() {
17107
- return existsSync12(join23(ROOT8, "package-lock.json")) || existsSync12(join23(ROOT8, "bun.lock")) || existsSync12(join23(ROOT8, "bun.lockb"));
17207
+ return existsSync13(join24(ROOT8, "package-lock.json")) || existsSync13(join24(ROOT8, "bun.lock")) || existsSync13(join24(ROOT8, "bun.lockb"));
17108
17208
  }
17109
17209
  var ROOT8;
17110
17210
  var init_release = __esm(() => {
17111
17211
  init_version();
17112
- ROOT8 = join23(fileURLToPath13(new URL(".", import.meta.url)), "..");
17212
+ ROOT8 = join24(fileURLToPath13(new URL(".", import.meta.url)), "..");
17113
17213
  });
17114
17214
 
17115
17215
  // src/dependency.ts
17116
17216
  import { readFileSync as readFileSync28 } from "node:fs";
17117
- import { join as join24 } from "node:path";
17217
+ import { join as join25 } from "node:path";
17118
17218
  import { fileURLToPath as fileURLToPath14 } from "node:url";
17119
17219
  function checkDependencies() {
17120
17220
  let pkg = {};
17121
17221
  try {
17122
- pkg = JSON.parse(readFileSync28(join24(ROOT9, "package.json"), "utf8"));
17222
+ pkg = JSON.parse(readFileSync28(join25(ROOT9, "package.json"), "utf8"));
17123
17223
  } catch {
17124
17224
  return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
17125
17225
  }
@@ -17142,12 +17242,12 @@ function checkDependencies() {
17142
17242
  }
17143
17243
  var ROOT9;
17144
17244
  var init_dependency = __esm(() => {
17145
- ROOT9 = join24(fileURLToPath14(new URL(".", import.meta.url)), "..");
17245
+ ROOT9 = join25(fileURLToPath14(new URL(".", import.meta.url)), "..");
17146
17246
  });
17147
17247
 
17148
17248
  // src/cli.ts
17149
- import { readFileSync as readFileSync29, existsSync as existsSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
17150
- import { join as join25, dirname as dirname5 } from "node:path";
17249
+ import { readFileSync as readFileSync29, existsSync as existsSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10 } from "node:fs";
17250
+ import { join as join26, dirname as dirname5 } from "node:path";
17151
17251
  import { homedir as homedir11 } from "node:os";
17152
17252
  import { fileURLToPath as fileURLToPath15 } from "node:url";
17153
17253
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -17192,7 +17292,7 @@ async function runDoctor() {
17192
17292
  detail: h1 ? `X-HackerOne-Research: ${h1}` : "H1_USERNAME not set",
17193
17293
  fix: h1 ? undefined : "export H1_USERNAME=<your-hackerone-username> (adds X-HackerOne-Research to outbound requests)"
17194
17294
  });
17195
- const dataOk = existsSync13(join25(ROOT10, "chains.json")) && existsSync13(join25(ROOT10, "tools-catalog.json"));
17295
+ const dataOk = existsSync14(join26(ROOT10, "chains.json")) && existsSync14(join26(ROOT10, "tools-catalog.json"));
17196
17296
  issues.push({
17197
17297
  name: "Data layers (chains + tools-catalog)",
17198
17298
  status: dataOk ? "ok" : "fail",
@@ -17243,15 +17343,15 @@ BlitzStrike — ${verdict}`);
17243
17343
  function resolveCommand() {
17244
17344
  if (which("blitzstrike"))
17245
17345
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
17246
- const src = join25(ROOT10, "src", "index.ts");
17247
- if (which("bun") && existsSync13(src))
17346
+ const src = join26(ROOT10, "src", "index.ts");
17347
+ if (which("bun") && existsSync14(src))
17248
17348
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
17249
17349
  if (which("npx"))
17250
17350
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
17251
17351
  if (which("bunx"))
17252
17352
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
17253
- const dist = join25(ROOT10, "dist", "index.js");
17254
- if (existsSync13(dist))
17353
+ const dist = join26(ROOT10, "dist", "index.js");
17354
+ if (existsSync14(dist))
17255
17355
  return { command: "bun", args: [dist, "serve", "--mcp"] };
17256
17356
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
17257
17357
  }
@@ -17265,7 +17365,7 @@ function readJson(p) {
17265
17365
  function jsonMcpServersWrite(p) {
17266
17366
  return (command, args) => {
17267
17367
  const dir = dirname5(p);
17268
- if (!existsSync13(dir))
17368
+ if (!existsSync14(dir))
17269
17369
  mkdirSync10(dir, { recursive: true });
17270
17370
  const existing = readJson(p) ?? {};
17271
17371
  existing.mcpServers = {
@@ -17278,7 +17378,7 @@ function jsonMcpServersWrite(p) {
17278
17378
  function opencodeWrite(p) {
17279
17379
  return (command, args) => {
17280
17380
  const dir = dirname5(p);
17281
- if (!existsSync13(dir))
17381
+ if (!existsSync14(dir))
17282
17382
  mkdirSync10(dir, { recursive: true });
17283
17383
  const existing = readJson(p) ?? {};
17284
17384
  existing.mcp = {
@@ -17290,16 +17390,16 @@ function opencodeWrite(p) {
17290
17390
  };
17291
17391
  }
17292
17392
  function copyOpenCodeAgents() {
17293
- const srcDir = join25(ROOT10, "opencode-agents");
17294
- const dstDir = join25(homedir11(), ".config", "opencode", "agents");
17295
- if (!existsSync13(srcDir))
17393
+ const srcDir = join26(ROOT10, "opencode-agents");
17394
+ const dstDir = join26(homedir11(), ".config", "opencode", "agents");
17395
+ if (!existsSync14(srcDir))
17296
17396
  return;
17297
- if (!existsSync13(dstDir))
17397
+ if (!existsSync14(dstDir))
17298
17398
  mkdirSync10(dstDir, { recursive: true });
17299
17399
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
17300
- const src = join25(srcDir, f);
17301
- const dst = join25(dstDir, f);
17302
- if (existsSync13(src)) {
17400
+ const src = join26(srcDir, f);
17401
+ const dst = join26(dstDir, f);
17402
+ if (existsSync14(src)) {
17303
17403
  writeFileSync9(dst, readFileSync29(src, "utf8"));
17304
17404
  }
17305
17405
  }
@@ -17307,9 +17407,9 @@ function copyOpenCodeAgents() {
17307
17407
  function codexWrite(p) {
17308
17408
  return (command, args) => {
17309
17409
  const dir = dirname5(p);
17310
- if (!existsSync13(dir))
17410
+ if (!existsSync14(dir))
17311
17411
  mkdirSync10(dir, { recursive: true });
17312
- let existing = existsSync13(p) ? readFileSync29(p, "utf8") : "";
17412
+ let existing = existsSync14(p) ? readFileSync29(p, "utf8") : "";
17313
17413
  if (!existing.trimEnd().endsWith(`
17314
17414
  `))
17315
17415
  existing += `
@@ -17326,9 +17426,9 @@ args = ${JSON.stringify(args)}
17326
17426
  function hermesWrite(p) {
17327
17427
  return (command, args) => {
17328
17428
  const dir = dirname5(p);
17329
- if (!existsSync13(dir))
17429
+ if (!existsSync14(dir))
17330
17430
  mkdirSync10(dir, { recursive: true });
17331
- let existing = existsSync13(p) ? readFileSync29(p, "utf8") : "";
17431
+ let existing = existsSync14(p) ? readFileSync29(p, "utf8") : "";
17332
17432
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
17333
17433
  if (!existing.trimEnd().endsWith(`
17334
17434
  `))
@@ -17355,74 +17455,74 @@ mcp_servers:
17355
17455
  function detectAgents() {
17356
17456
  const home = homedir11();
17357
17457
  const agents = [];
17358
- const claudeJson = join25(home, ".claude.json");
17458
+ const claudeJson = join26(home, ".claude.json");
17359
17459
  agents.push({
17360
17460
  name: "Claude Code",
17361
17461
  detectPath: claudeJson,
17362
- installed: () => existsSync13(claudeJson) || which("claude"),
17462
+ installed: () => existsSync14(claudeJson) || which("claude"),
17363
17463
  write: jsonMcpServersWrite(claudeJson)
17364
17464
  });
17365
- const claudeDesktop = join25(home, ".config", "Claude", "claude_desktop_config.json");
17465
+ const claudeDesktop = join26(home, ".config", "Claude", "claude_desktop_config.json");
17366
17466
  agents.push({
17367
17467
  name: "Claude Desktop",
17368
17468
  detectPath: claudeDesktop,
17369
- installed: () => existsSync13(claudeDesktop),
17469
+ installed: () => existsSync14(claudeDesktop),
17370
17470
  write: jsonMcpServersWrite(claudeDesktop)
17371
17471
  });
17372
- const cursor = join25(home, ".cursor", "mcp.json");
17472
+ const cursor = join26(home, ".cursor", "mcp.json");
17373
17473
  agents.push({
17374
17474
  name: "Cursor",
17375
17475
  detectPath: cursor,
17376
- installed: () => existsSync13(join25(home, ".cursor")) || which("cursor"),
17476
+ installed: () => existsSync14(join26(home, ".cursor")) || which("cursor"),
17377
17477
  write: jsonMcpServersWrite(cursor)
17378
17478
  });
17379
- const opencode = join25(home, ".config", "opencode", "opencode.jsonc");
17479
+ const opencode = join26(home, ".config", "opencode", "opencode.jsonc");
17380
17480
  agents.push({
17381
17481
  name: "OpenCode",
17382
17482
  detectPath: opencode,
17383
- installed: () => existsSync13(join25(home, ".config", "opencode")) || which("opencode"),
17483
+ installed: () => existsSync14(join26(home, ".config", "opencode")) || which("opencode"),
17384
17484
  write: opencodeWrite(opencode)
17385
17485
  });
17386
- const codex = join25(home, ".codex", "config.toml");
17486
+ const codex = join26(home, ".codex", "config.toml");
17387
17487
  agents.push({
17388
17488
  name: "Codex",
17389
17489
  detectPath: codex,
17390
- installed: () => existsSync13(join25(home, ".codex")) || which("codex"),
17490
+ installed: () => existsSync14(join26(home, ".codex")) || which("codex"),
17391
17491
  write: codexWrite(codex)
17392
17492
  });
17393
- const hermes = join25(home, ".hermes", "config.yaml");
17493
+ const hermes = join26(home, ".hermes", "config.yaml");
17394
17494
  agents.push({
17395
17495
  name: "Hermes",
17396
17496
  detectPath: hermes,
17397
- installed: () => existsSync13(join25(home, ".hermes")) || which("hermes"),
17497
+ installed: () => existsSync14(join26(home, ".hermes")) || which("hermes"),
17398
17498
  write: hermesWrite(hermes)
17399
17499
  });
17400
- const gemini = join25(home, ".gemini", "settings.json");
17500
+ const gemini = join26(home, ".gemini", "settings.json");
17401
17501
  agents.push({
17402
17502
  name: "Gemini CLI",
17403
17503
  detectPath: gemini,
17404
- installed: () => existsSync13(join25(home, ".gemini")) || which("gemini"),
17504
+ installed: () => existsSync14(join26(home, ".gemini")) || which("gemini"),
17405
17505
  write: jsonMcpServersWrite(gemini)
17406
17506
  });
17407
- const windsurf = join25(home, ".codeium", "windsurf", "mcp_config.json");
17507
+ const windsurf = join26(home, ".codeium", "windsurf", "mcp_config.json");
17408
17508
  agents.push({
17409
17509
  name: "Windsurf",
17410
17510
  detectPath: windsurf,
17411
- installed: () => existsSync13(join25(home, ".codeium")) || which("windsurf"),
17511
+ installed: () => existsSync14(join26(home, ".codeium")) || which("windsurf"),
17412
17512
  write: jsonMcpServersWrite(windsurf)
17413
17513
  });
17414
- const copilot = join25(home, ".copilot", "mcp.json");
17514
+ const copilot = join26(home, ".copilot", "mcp.json");
17415
17515
  agents.push({
17416
17516
  name: "Copilot",
17417
17517
  detectPath: copilot,
17418
- installed: () => existsSync13(join25(home, ".copilot")),
17518
+ installed: () => existsSync14(join26(home, ".copilot")),
17419
17519
  write: jsonMcpServersWrite(copilot)
17420
17520
  });
17421
- const cline = join25(home, ".cline", "mcp_settings.json");
17521
+ const cline = join26(home, ".cline", "mcp_settings.json");
17422
17522
  agents.push({
17423
17523
  name: "Cline",
17424
17524
  detectPath: cline,
17425
- installed: () => existsSync13(join25(home, ".cline")),
17525
+ installed: () => existsSync14(join26(home, ".cline")),
17426
17526
  write: jsonMcpServersWrite(cline)
17427
17527
  });
17428
17528
  return agents;
@@ -17540,7 +17640,7 @@ var init_cli = __esm(() => {
17540
17640
  init_release();
17541
17641
  init_dependency();
17542
17642
  init_mcp_status();
17543
- ROOT10 = join25(fileURLToPath15(new URL(".", import.meta.url)), "..");
17643
+ ROOT10 = join26(fileURLToPath15(new URL(".", import.meta.url)), "..");
17544
17644
  });
17545
17645
 
17546
17646
  // node_modules/zod/v3/helpers/util.js
@@ -57779,7 +57879,16 @@ var ALIASES = {
57779
57879
  "header injection": "crlf_injection",
57780
57880
  "http response splitting": "crlf_injection",
57781
57881
  "wrong sanitizer": "wrong_sanitizer",
57782
- "wrong context sanitization": "wrong_sanitizer"
57882
+ "wrong context sanitization": "wrong_sanitizer",
57883
+ "dependency confusion": "dependency_confusion",
57884
+ "dep confusion": "dependency_confusion",
57885
+ "supply chain": "dependency_confusion",
57886
+ "supply chain attack": "dependency_confusion",
57887
+ "ml supply chain": "ml_supply_chain",
57888
+ "ai supply chain": "ml_supply_chain",
57889
+ "model poisoning": "ml_supply_chain",
57890
+ "ml security": "ml_supply_chain",
57891
+ "ai ml security": "ml_supply_chain"
57783
57892
  };
57784
57893
  function normalize(term) {
57785
57894
  return term.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
@@ -57868,6 +57977,185 @@ function submissionGate(input) {
57868
57977
  };
57869
57978
  }
57870
57979
 
57980
+ // src/git-history.ts
57981
+ init_complex_bugs();
57982
+ import { execFileSync as execFileSync4 } from "node:child_process";
57983
+ import { existsSync as existsSync10 } from "node:fs";
57984
+ import { join as join17 } from "node:path";
57985
+ function parseDeletions(output) {
57986
+ const out = [];
57987
+ let commit = "";
57988
+ let msg = "";
57989
+ for (const line of output.split(`
57990
+ `)) {
57991
+ if (line.startsWith("__COMMIT__ ")) {
57992
+ const rest = line.slice("__COMMIT__ ".length);
57993
+ const sp = rest.indexOf(" ");
57994
+ commit = sp === -1 ? rest : rest.slice(0, sp);
57995
+ msg = sp === -1 ? "" : rest.slice(sp + 1);
57996
+ } else if (line.startsWith("D\t") || line.startsWith("D ")) {
57997
+ const path = line.slice(2).trim();
57998
+ if (path && commit)
57999
+ out.push({ commit, commit_msg: msg, path });
58000
+ }
58001
+ }
58002
+ return out;
58003
+ }
58004
+ function blobBeforeDeletion(root, commit, path) {
58005
+ return execFileSync4("git", ["-C", root, "show", `${commit}^:${path}`], {
58006
+ encoding: "utf8",
58007
+ maxBuffer: 10 * 1024 * 1024
58008
+ });
58009
+ }
58010
+ function scanGitHistory(root, opts) {
58011
+ const limit = opts?.limit ?? 30;
58012
+ const findings = [];
58013
+ if (!existsSync10(join17(root, ".git"))) {
58014
+ return { root, is_git_repo: false, total: 0, findings: [], error: "not a git repository (no .git dir)" };
58015
+ }
58016
+ if (opts?.secretPattern) {
58017
+ try {
58018
+ const pick = execFileSync4("git", ["-C", root, "log", "-S", opts.secretPattern, "--all", "--format=__COMMIT__ %H %s"], { encoding: "utf8", maxBuffer: 5 * 1024 * 1024 });
58019
+ const commits = pick.split(`
58020
+ `).filter(Boolean).map((l) => {
58021
+ const rest = l.startsWith("__COMMIT__ ") ? l.slice("__COMMIT__ ".length) : l;
58022
+ const sp = rest.indexOf(" ");
58023
+ return { hash: sp === -1 ? rest : rest.slice(0, sp), msg: sp === -1 ? "" : rest.slice(sp + 1) };
58024
+ });
58025
+ return {
58026
+ root,
58027
+ is_git_repo: true,
58028
+ mode: "pickaxe",
58029
+ pattern: opts.secretPattern,
58030
+ total: commits.length,
58031
+ commits,
58032
+ hint: "Commits where the pattern was added or removed — inspect each to recover the secret value."
58033
+ };
58034
+ } catch (e) {
58035
+ return { root, is_git_repo: true, mode: "pickaxe", pattern: opts.secretPattern, total: 0, error: String(e) };
58036
+ }
58037
+ }
58038
+ try {
58039
+ 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 });
58040
+ const deletions = parseDeletions(log);
58041
+ const seen = new Set;
58042
+ let checked = 0;
58043
+ for (const d of deletions) {
58044
+ if (findings.length >= limit)
58045
+ break;
58046
+ const key = `${d.path}`;
58047
+ if (seen.has(key))
58048
+ continue;
58049
+ seen.add(key);
58050
+ try {
58051
+ const blob = blobBeforeDeletion(root, d.commit, d.path);
58052
+ checked++;
58053
+ const hits = detectComplexBugs(blob, d.path).filter((f) => f.type === "hardcoded_secret");
58054
+ for (const h of hits) {
58055
+ findings.push({
58056
+ path: d.path,
58057
+ commit: d.commit,
58058
+ commit_msg: d.commit_msg,
58059
+ severity: h.severity,
58060
+ category: h.category,
58061
+ evidence: h.evidence,
58062
+ 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.`
58063
+ });
58064
+ }
58065
+ } catch {}
58066
+ }
58067
+ return {
58068
+ root,
58069
+ is_git_repo: true,
58070
+ mode: "deleted_files",
58071
+ deletion_commits: deletions.length,
58072
+ blobs_checked: checked,
58073
+ total: findings.length,
58074
+ findings,
58075
+ hint: "Secrets in deleted files remain in .git history. Rotate any recovered credential and rewrite/scrub history."
58076
+ };
58077
+ } catch (e) {
58078
+ return { root, is_git_repo: true, mode: "deleted_files", total: 0, findings: [], error: String(e) };
58079
+ }
58080
+ }
58081
+
58082
+ // src/key-validation.ts
58083
+ function parseValueFromBlob(text, varName) {
58084
+ const re = new RegExp(`${varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[=:]\\s*["']?([^"'\\n]+)`);
58085
+ const m = text.match(re);
58086
+ if (!m)
58087
+ throw new Error(`variable ${varName} not found in blob`);
58088
+ return m[1].trim();
58089
+ }
58090
+ async function httpGet3(url, headers) {
58091
+ try {
58092
+ const res = await fetch(url, { headers, redirect: "follow" });
58093
+ return { status: res.status, body: await res.text() };
58094
+ } catch (e) {
58095
+ return { status: null, body: String(e) };
58096
+ }
58097
+ }
58098
+ async function validateLeakedKey(input) {
58099
+ const endpoints = input.endpoints ?? ["/items"];
58100
+ const scheme = input.scheme ?? "Bearer {value}";
58101
+ const header = input.header ?? "Authorization";
58102
+ let key = input.key ?? "";
58103
+ if (!key && input.blobUrl) {
58104
+ const blobRes = await httpGet3(input.blobUrl, { "User-Agent": "Mozilla/5.0 (recon-agent)" });
58105
+ if (blobRes.status !== 200) {
58106
+ return { ok: false, error: `blob fetch failed (HTTP ${blobRes.status}) — leak gone, privatized, or wrong sha`, step: "blob_fetch" };
58107
+ }
58108
+ try {
58109
+ key = parseValueFromBlob(blobRes.body, input.varName ?? "SECRET_KEY");
58110
+ } catch (e) {
58111
+ return { ok: false, error: String(e), step: "parse" };
58112
+ }
58113
+ }
58114
+ if (!key) {
58115
+ return { ok: false, error: "no key provided and no blobUrl to recover one from", step: "key" };
58116
+ }
58117
+ const authHeader = { [header]: scheme.replace("{value}", key) };
58118
+ const results = [];
58119
+ let proven = false;
58120
+ for (const ep of endpoints) {
58121
+ const url = input.apiBase.replace(/\/$/, "") + (ep.startsWith("/") ? ep : "/" + ep);
58122
+ const r0 = await httpGet3(url, { "User-Agent": "Mozilla/5.0 (recon-agent)" });
58123
+ const r1 = await httpGet3(url, { "User-Agent": "Mozilla/5.0 (recon-agent)", ...authHeader });
58124
+ let verdict = "-";
58125
+ if (r0.status === null || r1.status === null)
58126
+ verdict = "ERROR";
58127
+ else if ((r0.status === 401 || r0.status === 403) && r1.status === 200) {
58128
+ verdict = "BYPASS";
58129
+ proven = true;
58130
+ }
58131
+ let records = null;
58132
+ if (r1.status === 200) {
58133
+ try {
58134
+ records = (JSON.parse(r1.body).data ?? []).length;
58135
+ } catch {
58136
+ records = null;
58137
+ }
58138
+ }
58139
+ results.push({
58140
+ endpoint: ep,
58141
+ no_auth: r0.status,
58142
+ with_key: r1.status,
58143
+ records,
58144
+ verdict,
58145
+ error: verdict === "ERROR" ? r0.status === null ? r0.body : r1.body : undefined
58146
+ });
58147
+ }
58148
+ return {
58149
+ ok: true,
58150
+ proven,
58151
+ key_len: key.length,
58152
+ header,
58153
+ scheme,
58154
+ results,
58155
+ verdict: proven ? "AUTH BYPASS PROVEN — leaked credential valid on live system (read-only)" : "not proven — rotated? different header scheme? check manually"
58156
+ };
58157
+ }
58158
+
57871
58159
  // src/severity.ts
57872
58160
  var VRT_MAP = {
57873
58161
  sql_injection: { vrt: "injection > sql_injection", priority: "P1", severity: "critical", rationale: "SQL injection reaches sensitive data or enables admin takeover." },
@@ -57885,7 +58173,13 @@ var VRT_MAP = {
57885
58173
  prototype_pollution: { vrt: "injection > prototype_pollution", priority: "P2", severity: "high", rationale: "Object prototype corruption can escalate to code execution in gadget-heavy runtimes." },
57886
58174
  crlf_injection: { vrt: "injection > crlf_injection", priority: "P3", severity: "medium", rationale: "Header injection enables cookie poisoning or cache poisoning." },
57887
58175
  missing_nonce: { vrt: "broken_access_control > cross_site_request_forgery", priority: "P3", severity: "medium", rationale: "A state-changing action lacks an anti-CSRF token." },
57888
- wrong_sanitizer: { vrt: "injection > improper_input_validation", priority: "P3", severity: "medium", rationale: "A value sanitized for the wrong context still reaches the sink; severity follows the sink." }
58176
+ wrong_sanitizer: { vrt: "injection > improper_input_validation", priority: "P3", severity: "medium", rationale: "A value sanitized for the wrong context still reaches the sink; severity follows the sink." },
58177
+ llm_injection: { vrt: "injection > prompt_injection", priority: "P2", severity: "high", rationale: "Prompt injection can exfiltrate the system prompt, RAG data, or trigger privileged tool use." },
58178
+ graphql_exposure: { vrt: "misconfiguration > graphql_introspection", priority: "P4", severity: "low", rationale: "GraphQL introspection exposes the schema but is not directly exploitable." },
58179
+ oauth_misconfig: { vrt: "authentication > oauth_misconfiguration", priority: "P2", severity: "high", rationale: "An OAuth/open-redirect misconfiguration can leak authorization codes → account takeover." },
58180
+ grpc_reflection: { vrt: "misconfiguration > debug_information_exposure", priority: "P5", severity: "low", rationale: "gRPC reflection exposes the API contract but is not directly exploitable." },
58181
+ dependency_confusion: { vrt: "insecure_design > dependency_confusion", priority: "P2", severity: "high", rationale: "An internal package resolves from a squattable public registry → attacker-controlled code in the build." },
58182
+ ml_supply_chain: { vrt: "injection > insecure_deserialization", priority: "P1", severity: "critical", rationale: "Untrusted model loading via pickle (torch/joblib/numpy) → arbitrary code execution on load." }
57889
58183
  };
57890
58184
  function severityCalibrate(input) {
57891
58185
  const type = input.type?.toLowerCase() ?? "";
@@ -57925,12 +58219,12 @@ function listSeverityMap() {
57925
58219
 
57926
58220
  // src/datarefresh.ts
57927
58221
  import { readFileSync as readFileSync18 } from "node:fs";
57928
- import { join as join17, dirname as dirname3 } from "node:path";
58222
+ import { join as join18, dirname as dirname3 } from "node:path";
57929
58223
  import { fileURLToPath as fileURLToPath9 } from "node:url";
57930
58224
  var __dirname3 = dirname3(fileURLToPath9(import.meta.url));
57931
58225
  function loadJson2(rel) {
57932
58226
  try {
57933
- return JSON.parse(readFileSync18(join17(__dirname3, "..", rel), "utf8"));
58227
+ return JSON.parse(readFileSync18(join18(__dirname3, "..", rel), "utf8"));
57934
58228
  } catch {
57935
58229
  return null;
57936
58230
  }
@@ -57966,13 +58260,13 @@ init_variants();
57966
58260
 
57967
58261
  // src/benchmark.ts
57968
58262
  import { readFileSync as readFileSync20, readdirSync as readdirSync5, writeFileSync as writeFileSync6, rmSync as rmSync3, mkdirSync as mkdirSync7 } from "node:fs";
57969
- import { join as join19 } from "node:path";
58263
+ import { join as join20 } from "node:path";
57970
58264
  import { fileURLToPath as fileURLToPath11 } from "node:url";
57971
58265
  import { tmpdir as tmpdir3, homedir as homedir8 } from "node:os";
57972
58266
  init_complex_bugs();
57973
- var ROOT5 = join19(fileURLToPath11(new URL(".", import.meta.url)), "..");
57974
- var CORPUS = join19(ROOT5, "bench", "corpus");
57975
- var CAPTURED_CORPUS_DIR = process.env.BLITZSTRIKE_CORPUS_DIR ?? join19(homedir8(), ".blitzstrike", "corpus");
58267
+ var ROOT5 = join20(fileURLToPath11(new URL(".", import.meta.url)), "..");
58268
+ var CORPUS = join20(ROOT5, "bench", "corpus");
58269
+ var CAPTURED_CORPUS_DIR = process.env.BLITZSTRIKE_CORPUS_DIR ?? join20(homedir8(), ".blitzstrike", "corpus");
57976
58270
  function parseCorpusFile(path) {
57977
58271
  let text;
57978
58272
  try {
@@ -58032,7 +58326,7 @@ function loadDir(dir) {
58032
58326
  for (const f of readdirSync5(dir)) {
58033
58327
  if (!f.endsWith(".md"))
58034
58328
  continue;
58035
- const e = parseCorpusFile(join19(dir, f));
58329
+ const e = parseCorpusFile(join20(dir, f));
58036
58330
  if (e)
58037
58331
  entries.push(e);
58038
58332
  }
@@ -58059,7 +58353,7 @@ function captureFalsePositive(input) {
58059
58353
  input.code,
58060
58354
  "```"
58061
58355
  ].filter((l) => l !== null);
58062
- const path = join19(CAPTURED_CORPUS_DIR, `${id}.md`);
58356
+ const path = join20(CAPTURED_CORPUS_DIR, `${id}.md`);
58063
58357
  writeFileSync6(path, lines.join(`
58064
58358
  `) + `
58065
58359
  `, "utf8");
@@ -58081,7 +58375,7 @@ function detect(entry) {
58081
58375
  }
58082
58376
  const lang = entry.language;
58083
58377
  if (lang === "php") {
58084
- const tmp = join19(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
58378
+ const tmp = join20(tmpdir3(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
58085
58379
  writeFileSync6(tmp, entry.code);
58086
58380
  const r = analyzeDataFlow2(tmp);
58087
58381
  rmSync3(tmp, { force: true });
@@ -58150,21 +58444,21 @@ function runBenchmark() {
58150
58444
  }
58151
58445
 
58152
58446
  // src/obligations.ts
58153
- import { readFileSync as readFileSync21, appendFileSync as appendFileSync4, mkdirSync as mkdirSync8, existsSync as existsSync10, writeFileSync as writeFileSync7 } from "node:fs";
58154
- import { join as join20 } from "node:path";
58447
+ import { readFileSync as readFileSync21, appendFileSync as appendFileSync4, mkdirSync as mkdirSync8, existsSync as existsSync11, writeFileSync as writeFileSync7 } from "node:fs";
58448
+ import { join as join21 } from "node:path";
58155
58449
  import { createHash as createHash6 } from "node:crypto";
58156
58450
  import { homedir as homedir9 } from "node:os";
58157
- var HOME_DIR4 = process.env.BLITZSTRIKE_HOME ?? join20(homedir9(), ".blitzstrike");
58158
- var OBLIGATION_PATH = join20(HOME_DIR4, "obligations.jsonl");
58451
+ var HOME_DIR4 = process.env.BLITZSTRIKE_HOME ?? join21(homedir9(), ".blitzstrike");
58452
+ var OBLIGATION_PATH = join21(HOME_DIR4, "obligations.jsonl");
58159
58453
  function ensureDir3() {
58160
- if (!existsSync10(HOME_DIR4))
58454
+ if (!existsSync11(HOME_DIR4))
58161
58455
  mkdirSync8(HOME_DIR4, { recursive: true });
58162
58456
  }
58163
58457
  function shortId2(claim) {
58164
58458
  return `obl-${createHash6("sha256").update(`${claim}\x00${Date.now()}`).digest("hex").slice(0, 12)}`;
58165
58459
  }
58166
58460
  function loadObligations() {
58167
- if (!existsSync10(OBLIGATION_PATH))
58461
+ if (!existsSync11(OBLIGATION_PATH))
58168
58462
  return [];
58169
58463
  try {
58170
58464
  const out = [];
@@ -59167,17 +59461,17 @@ function analyzeJwt(token, opts = {}) {
59167
59461
  }
59168
59462
 
59169
59463
  // src/enterprise-benchmark.ts
59170
- import { existsSync as existsSync11, readFileSync as readFileSync24, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
59171
- import { join as join21, resolve } from "node:path";
59464
+ import { existsSync as existsSync12, readFileSync as readFileSync24, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
59465
+ import { join as join22, resolve } from "node:path";
59172
59466
  init_complex_bugs();
59173
59467
  var ROOT6 = resolve(process.cwd(), "bench", "enterprise");
59174
59468
  var ENTERPRISE_FIXTURES = [
59175
- { id: "php-laravel", dir: join21(ROOT6, "php-laravel"), expected_vulns: 4 },
59176
- { id: "js-express", dir: join21(ROOT6, "js-express"), expected_vulns: 2 },
59177
- { id: "py-django", dir: join21(ROOT6, "py-django"), expected_vulns: 2 },
59178
- { id: "java-spring", dir: join21(ROOT6, "java-spring"), expected_vulns: 2 },
59179
- { id: "wp-batch-route", dir: join21(ROOT6, "wp-batch-route"), expected_vulns: 3 },
59180
- { id: "php-complex", dir: join21(ROOT6, "php-complex"), expected_vulns: 4 }
59469
+ { id: "php-laravel", dir: join22(ROOT6, "php-laravel"), expected_vulns: 4 },
59470
+ { id: "js-express", dir: join22(ROOT6, "js-express"), expected_vulns: 2 },
59471
+ { id: "py-django", dir: join22(ROOT6, "py-django"), expected_vulns: 2 },
59472
+ { id: "java-spring", dir: join22(ROOT6, "java-spring"), expected_vulns: 2 },
59473
+ { id: "wp-batch-route", dir: join22(ROOT6, "wp-batch-route"), expected_vulns: 3 },
59474
+ { id: "php-complex", dir: join22(ROOT6, "php-complex"), expected_vulns: 4 }
59181
59475
  ];
59182
59476
  function sourceFiles(root) {
59183
59477
  const out = [];
@@ -59189,7 +59483,7 @@ function sourceFiles(root) {
59189
59483
  return;
59190
59484
  }
59191
59485
  for (const e of entries) {
59192
- const p = join21(dir, e);
59486
+ const p = join22(dir, e);
59193
59487
  let st;
59194
59488
  try {
59195
59489
  st = statSync3(p);
@@ -59228,7 +59522,7 @@ function detectFixture(dir) {
59228
59522
  function runEnterpriseBenchmark() {
59229
59523
  const fixtures = [];
59230
59524
  for (const fx of ENTERPRISE_FIXTURES) {
59231
- if (!existsSync11(fx.dir)) {
59525
+ if (!existsSync12(fx.dir)) {
59232
59526
  fixtures.push({ id: fx.id, available: false, detected: 0, expected: fx.expected_vulns, recall: 0 });
59233
59527
  continue;
59234
59528
  }
@@ -59405,7 +59699,7 @@ function createServer() {
59405
59699
  ` + `- 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).
59406
59700
  ` + `- 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.
59407
59701
 
59408
- ` + "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:
59702
+ ` + "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). Two supply-chain detectors: dependency_confusion (a manifest/build file resolving a package from a public registry — pypi.org/npmjs.org --extra-index-url, or a scoped @company/pkg with no private registry) and ml_supply_chain (torch.load without weights_only=True / joblib.load / np.load(allow_pickle=True) / keras load_model on an untrusted model → pickle RCE).\n" + `PRECISION ANALYSIS — chrome-devtools MCP is MANDATORY, not optional:
59409
59703
  ` + `- 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.
59410
59704
  ` + `- 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.
59411
59705
  ` + "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."
@@ -59849,6 +60143,32 @@ ${statusLine}` : statusLine },
59849
60143
  const r = synonymsFor(term);
59850
60144
  return { content: [{ type: "text", text: JSON.stringify({ ...r, pattern: patternLookup(r.canonical) }) }] };
59851
60145
  });
60146
+ server.registerTool("scan_git_history", {
60147
+ title: "Mine git history for deleted secrets",
60148
+ 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.",
60149
+ inputSchema: {
60150
+ root: string2().describe("Path to the git repo root (must contain .git)."),
60151
+ secretPattern: string2().optional().describe("Optional: a specific secret token to pickaxe-search (git log -S) through history."),
60152
+ limit: number2().int().optional().describe("Max findings (default 30).")
60153
+ }
60154
+ }, async ({ root, secretPattern, limit }) => {
60155
+ return { content: [{ type: "text", text: JSON.stringify(scanGitHistory(root, { secretPattern, limit })) }] };
60156
+ });
60157
+ server.registerTool("validate_leaked_key", {
60158
+ title: "Prove a leaked key is still active (differential)",
60159
+ 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.",
60160
+ inputSchema: {
60161
+ key: string2().optional().describe("Raw secret value to test (skip blob fetch)."),
60162
+ blobUrl: string2().optional().describe("Or: raw-blob URL (e.g. forge raw/commit/<sha>^/<path>) to fetch + parse the key from."),
60163
+ varName: string2().optional().describe("Variable name to parse from the blob (default SECRET_KEY)."),
60164
+ apiBase: string2().describe("Live API base URL."),
60165
+ endpoints: array(string2()).optional().describe("Read-only GET endpoints to probe (default ['/items'])."),
60166
+ scheme: string2().optional().describe("Header value template, e.g. 'Bearer {value}' (default)."),
60167
+ header: string2().optional().describe("Header name (default Authorization).")
60168
+ }
60169
+ }, async (input) => {
60170
+ return { content: [{ type: "text", text: JSON.stringify(await validateLeakedKey(input)) }] };
60171
+ });
59852
60172
  server.registerTool("fofa_search", {
59853
60173
  title: "FOFA asset search",
59854
60174
  description: "STRIKE: search the FOFA asset index (needs FOFA_EMAIL + FOFA_KEY).",
@@ -61193,14 +61513,14 @@ async function serve() {
61193
61513
 
61194
61514
  // src/index.ts
61195
61515
  init_cli();
61196
- import { readFileSync as readFileSync30, existsSync as existsSync14 } from "node:fs";
61197
- import { join as join26 } from "node:path";
61516
+ import { readFileSync as readFileSync30, existsSync as existsSync15 } from "node:fs";
61517
+ import { join as join27 } from "node:path";
61198
61518
  import { fileURLToPath as fileURLToPath16 } from "node:url";
61199
- var ROOT11 = join26(fileURLToPath16(new URL(".", import.meta.url)), "..");
61519
+ var ROOT11 = join27(fileURLToPath16(new URL(".", import.meta.url)), "..");
61200
61520
  function readVersion() {
61201
61521
  try {
61202
- const p = join26(ROOT11, "package.json");
61203
- if (existsSync14(p))
61522
+ const p = join27(ROOT11, "package.json");
61523
+ if (existsSync15(p))
61204
61524
  return JSON.parse(readFileSync30(p, "utf8")).version ?? "1.0.0";
61205
61525
  } catch {}
61206
61526
  return "1.0.0";