blitzstrike 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to Blitz Strike are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [1.0.33] — 2026-09-13
9
+
10
+ ### Added
11
+
12
+ - **Phase 1 — Core Integrity** (per the master upgrade spec):
13
+ - **Finding invariants** (`src/invariants.ts`) — hard, machine-checkable rules:
14
+ a finding cannot be CONFIRMED without evidence, cannot be CONFIRMED with a
15
+ failed negative control, cannot be CONFIRMED out of scope, cannot persist a
16
+ raw secret, and its evidence hash must stay stable. Enforced in
17
+ `confirmFinding` and re-checkable via `checkFindingInvariants`.
18
+ - **Evidence provenance** (`src/evidence.ts` §16) — every evidence record now
19
+ carries chain-of-custody metadata (`tool`, `version`, `target`, `scope`,
20
+ `parentEvidence`).
21
+ - **Audit log** (`src/audit.ts` §45) — structured append-only audit events
22
+ (`scope_checked`, `finding_created`, `finding_updated`, `validation_finished`,
23
+ `evidence_created`, …) at `~/.blitzstrike/audit.jsonl`, wired into scope
24
+ check, finding creation, evidence attachment, and validation.
25
+ - `confirmFinding` now **throws** if called with no evidence (hard invariant).
26
+
27
+ ### Changed
28
+
29
+ - `scopeCheck` refactored to a single exit path that always records a
30
+ `scope_checked` audit event.
31
+ - Shared `src/version.ts` — single source of truth for the package version
32
+ (removed the duplicate VERSION readers in `server.ts`).
33
+
8
34
  ## [1.0.26] — 2026-09-13
9
35
 
10
36
  The first hardening milestone: the whole roadmap is implemented, measured, and
@@ -51,6 +77,9 @@ code path, not a slogan.
51
77
  master reference of web attack vectors (RCE, SQL, auth, SSRF, XSS, API,
52
78
  business logic, race conditions, cloud, supply chain, CI/CD, AI/LLM, Web3,
53
79
  XS-leaks, …) for BLITZ attack-surface mapping.
80
+ - **Self-update** (`blitzstrike update` + `check_update` tool) — query the npm
81
+ registry for the latest version (semver-aware) and refresh the heavy data
82
+ cache (payloads + templates) in one shot.
54
83
 
55
84
  ### Changed
56
85
 
package/README.md CHANGED
@@ -117,6 +117,8 @@ blitzstrike serve --mcp # start MCP server over stdio (default)
117
117
  blitzstrike doctor # health check: runtime + 130-tool catalog + creds
118
118
  blitzstrike install # write MCP config to detected clients (Claude/Cursor/OpenCode)
119
119
  blitzstrike install --dry-run # preview the config without writing
120
+ blitzstrike sync-data # fetch heavy datasets (payloads + templates) on-demand
121
+ blitzstrike update # check for a newer version + refresh the data cache
120
122
  blitzstrike version # print version
121
123
  ```
122
124
 
@@ -226,6 +228,7 @@ MCP servers. With no client detected, it prints the snippet for manual paste.
226
228
  | `template_lookup(topic)` | Find nuclei detection templates (11.9k YAML signatures). |
227
229
  | `list_attack_vectors()` | List the full web attack-vector taxonomy (34 categories, 588 vectors). |
228
230
  | `attack_vectors(category)` | Enumerate the vector list for one category (e.g. ssrf, business logic, ai/llm). |
231
+ | `check_update()` | Query the npm registry for the latest blitzstrike version. |
229
232
 
230
233
  The intelligence layer (WAF signatures, tech/CVE/port correlations, fuzzer
231
234
  data, vuln ontology, exploit payloads, nuclei detection templates) is sourced
package/dist/index.js CHANGED
@@ -14820,11 +14820,11 @@ var require_src = __commonJS(function(exports, module) {
14820
14820
  });
14821
14821
 
14822
14822
  // src/sync.ts
14823
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync3, rmSync as rmSync2 } from "node:fs";
14824
- import { join as join9 } from "node:path";
14823
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync3, rmSync as rmSync2 } from "node:fs";
14824
+ import { join as join10 } from "node:path";
14825
14825
  import { homedir as homedir4 } from "node:os";
14826
14826
  import { fileURLToPath as fileURLToPath7 } from "node:url";
14827
- import { spawnSync as spawnSync3 } from "node:child_process";
14827
+ import { spawnSync as spawnSync2 } from "node:child_process";
14828
14828
  function dirExists(p) {
14829
14829
  try {
14830
14830
  return statSync3(p).isDirectory();
@@ -14838,7 +14838,7 @@ function countFiles(dir) {
14838
14838
  const walk = (d) => {
14839
14839
  for (const e of readdirSync5(d, { withFileTypes: true })) {
14840
14840
  if (e.isDirectory())
14841
- walk(join9(d, e.name));
14841
+ walk(join10(d, e.name));
14842
14842
  else
14843
14843
  n++;
14844
14844
  }
@@ -14852,8 +14852,8 @@ function countFiles(dir) {
14852
14852
  function syncData() {
14853
14853
  if (!dirExists(DATA_ROOT2))
14854
14854
  mkdirSync3(DATA_ROOT2, { recursive: true });
14855
- const payloadsPath = join9(DATA_ROOT2, "payloads");
14856
- const templatesPath = join9(DATA_ROOT2, "templates");
14855
+ const payloadsPath = join10(DATA_ROOT2, "payloads");
14856
+ const templatesPath = join10(DATA_ROOT2, "templates");
14857
14857
  const hasPayloads = dirExists(payloadsPath) && countFiles(payloadsPath) > 0;
14858
14858
  const hasTemplates = dirExists(templatesPath) && countFiles(templatesPath) > 0;
14859
14859
  if (hasPayloads && hasTemplates) {
@@ -14863,8 +14863,8 @@ function syncData() {
14863
14863
  data_root: DATA_ROOT2
14864
14864
  };
14865
14865
  }
14866
- const localPayloads = join9(ROOT6, "payloads");
14867
- const localTemplates = join9(ROOT6, "templates");
14866
+ const localPayloads = join10(ROOT5, "payloads");
14867
+ const localTemplates = join10(ROOT5, "templates");
14868
14868
  let payloadsSynced = false;
14869
14869
  let templatesSynced = false;
14870
14870
  if (!hasPayloads && dirExists(localPayloads)) {
@@ -14875,21 +14875,21 @@ function syncData() {
14875
14875
  copyDir(localTemplates, templatesPath);
14876
14876
  templatesSynced = true;
14877
14877
  }
14878
- const gitOk = spawnSync3("git", ["--version"]).status === 0;
14878
+ const gitOk = spawnSync2("git", ["--version"]).status === 0;
14879
14879
  if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
14880
- const tmp = join9(DATA_ROOT2, ".tmp-clone");
14880
+ const tmp = join10(DATA_ROOT2, ".tmp-clone");
14881
14881
  if (dirExists(tmp))
14882
14882
  rmSync2(tmp, { recursive: true, force: true });
14883
- const r = spawnSync3("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
14883
+ const r = spawnSync2("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
14884
14884
  if (r.status === 0) {
14885
- const sparse = spawnSync3("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
14885
+ const sparse = spawnSync2("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
14886
14886
  if (sparse.status === 0) {
14887
14887
  if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
14888
- copyDir(join9(tmp, "payloads"), payloadsPath);
14888
+ copyDir(join10(tmp, "payloads"), payloadsPath);
14889
14889
  payloadsSynced = true;
14890
14890
  }
14891
14891
  if (!dirExists(templatesPath) || countFiles(templatesPath) === 0) {
14892
- copyDir(join9(tmp, "templates"), templatesPath);
14892
+ copyDir(join10(tmp, "templates"), templatesPath);
14893
14893
  templatesSynced = true;
14894
14894
  }
14895
14895
  }
@@ -14903,7 +14903,7 @@ function syncData() {
14903
14903
  };
14904
14904
  }
14905
14905
  function copyDir(src, dst) {
14906
- const r = spawnSync3("cp", ["-r", src, dst], { timeout: 120000 });
14906
+ const r = spawnSync2("cp", ["-r", src, dst], { timeout: 120000 });
14907
14907
  if (r.status !== 0) {
14908
14908
  manualCopy(src, dst);
14909
14909
  }
@@ -14911,18 +14911,59 @@ function copyDir(src, dst) {
14911
14911
  function manualCopy(src, dst) {
14912
14912
  mkdirSync3(dst, { recursive: true });
14913
14913
  for (const e of readdirSync5(src, { withFileTypes: true })) {
14914
- const s = join9(src, e.name);
14915
- const d = join9(dst, e.name);
14914
+ const s = join10(src, e.name);
14915
+ const d = join10(dst, e.name);
14916
14916
  if (e.isDirectory())
14917
14917
  manualCopy(s, d);
14918
14918
  else
14919
- writeFileSync4(d, readFileSync11(s));
14919
+ writeFileSync3(d, readFileSync12(s));
14920
14920
  }
14921
14921
  }
14922
- var ROOT6, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
14922
+ var ROOT5, DATA_ROOT2, GITHUB_REPO = "https://github.com/shinthink/blitzstrike.git";
14923
14923
  var init_sync = __esm(() => {
14924
- ROOT6 = join9(fileURLToPath7(new URL(".", import.meta.url)), "..");
14925
- DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join9(homedir4(), ".blitzstrike", "data");
14924
+ ROOT5 = join10(fileURLToPath7(new URL(".", import.meta.url)), "..");
14925
+ DATA_ROOT2 = process.env.BLITZSTRIKE_DATA ?? join10(homedir4(), ".blitzstrike", "data");
14926
+ });
14927
+
14928
+ // src/update.ts
14929
+ async function checkLatestVersion(timeoutMs = 1e4) {
14930
+ try {
14931
+ const controller = new AbortController;
14932
+ const t = setTimeout(() => controller.abort(), timeoutMs);
14933
+ const res = await fetch("https://registry.npmjs.org/blitzstrike/latest", { signal: controller.signal });
14934
+ clearTimeout(t);
14935
+ if (!res.ok)
14936
+ return null;
14937
+ const data = await res.json();
14938
+ return data.version ?? null;
14939
+ } catch {
14940
+ return null;
14941
+ }
14942
+ }
14943
+ function semverCompare(a, b) {
14944
+ const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
14945
+ const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
14946
+ for (let i = 0;i < 3; i++) {
14947
+ const x = pa[i] ?? 0;
14948
+ const y = pb[i] ?? 0;
14949
+ if (x !== y)
14950
+ return x < y ? -1 : 1;
14951
+ }
14952
+ return 0;
14953
+ }
14954
+ async function runUpdate(currentVersion) {
14955
+ const latest = await checkLatestVersion();
14956
+ const outdated = latest !== null && semverCompare(currentVersion, latest) < 0;
14957
+ return {
14958
+ current: currentVersion,
14959
+ latest,
14960
+ outdated,
14961
+ update_command: "npm install -g blitzstrike@latest # or: bun add -g blitzstrike@latest",
14962
+ data_sync: syncData()
14963
+ };
14964
+ }
14965
+ var init_update = __esm(() => {
14966
+ init_sync();
14926
14967
  });
14927
14968
 
14928
14969
  // node_modules/zod/v3/helpers/util.js
@@ -29642,9 +29683,9 @@ async function nvdLookup(cveId) {
29642
29683
 
29643
29684
  // src/orchestrator.ts
29644
29685
  init_scanner();
29645
- import { readFileSync as readFileSync4, existsSync as existsSync3, statSync as statSync2 } from "node:fs";
29646
- import { join as join4 } from "node:path";
29647
- import { fileURLToPath as fileURLToPath2 } from "node:url";
29686
+ import { readFileSync as readFileSync6, existsSync as existsSync4, statSync as statSync2 } from "node:fs";
29687
+ import { join as join6 } from "node:path";
29688
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
29648
29689
 
29649
29690
  // src/memory.ts
29650
29691
  import { readFileSync as readFileSync2, appendFileSync, mkdirSync, existsSync, writeFileSync } from "node:fs";
@@ -29876,6 +29917,47 @@ function manualForTool(toolName) {
29876
29917
  return null;
29877
29918
  }
29878
29919
 
29920
+ // src/audit.ts
29921
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
29922
+ import { join as join5 } from "node:path";
29923
+ import { homedir as homedir2 } from "node:os";
29924
+
29925
+ // src/version.ts
29926
+ import { readFileSync as readFileSync4 } from "node:fs";
29927
+ import { join as join4, dirname } from "node:path";
29928
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
29929
+ var VERSION = (() => {
29930
+ try {
29931
+ const root = join4(dirname(fileURLToPath2(import.meta.url)), "..");
29932
+ return JSON.parse(readFileSync4(join4(root, "package.json"), "utf8")).version ?? "unknown";
29933
+ } catch {
29934
+ return "unknown";
29935
+ }
29936
+ })();
29937
+
29938
+ // src/audit.ts
29939
+ var HOME_DIR2 = process.env.BLITZSTRIKE_HOME ?? join5(homedir2(), ".blitzstrike");
29940
+ var AUDIT_PATH = join5(HOME_DIR2, "audit.jsonl");
29941
+ function recordAudit(event, opts = {}) {
29942
+ const record = {
29943
+ timestamp: new Date().toISOString(),
29944
+ event,
29945
+ actor: opts.actor ?? "system",
29946
+ tool: opts.tool,
29947
+ tool_version: VERSION,
29948
+ target: opts.target,
29949
+ result: opts.result,
29950
+ correlationId: opts.correlationId
29951
+ };
29952
+ try {
29953
+ if (!existsSync3(HOME_DIR2))
29954
+ mkdirSync2(HOME_DIR2, { recursive: true });
29955
+ appendFileSync2(AUDIT_PATH, JSON.stringify(record) + `
29956
+ `);
29957
+ } catch {}
29958
+ return record;
29959
+ }
29960
+
29879
29961
  // src/evidence.ts
29880
29962
  import { createHash as createHash2 } from "node:crypto";
29881
29963
  var REDACTION_PATTERNS = [
@@ -29902,6 +29984,13 @@ function redactSecrets(text) {
29902
29984
  }
29903
29985
  return out;
29904
29986
  }
29987
+ function hasSecrets(text) {
29988
+ for (const [re] of REDACTION_PATTERNS) {
29989
+ if (re.test(text))
29990
+ return true;
29991
+ }
29992
+ return false;
29993
+ }
29905
29994
  function redactObject(value) {
29906
29995
  if (typeof value === "string")
29907
29996
  return redactSecrets(value);
@@ -29936,9 +30025,61 @@ function makeEvidence(input) {
29936
30025
  source: input.source ? redactObject(input.source) : undefined,
29937
30026
  sink: input.sink ? redactObject(input.sink) : undefined,
29938
30027
  artifacts,
29939
- recorded: new Date().toISOString()
30028
+ recorded: new Date().toISOString(),
30029
+ provenance: {
30030
+ tool: input.provenance?.tool ?? "blitzstrike",
30031
+ version: input.provenance?.version ?? VERSION,
30032
+ ...input.provenance?.target ? { target: input.provenance.target } : {},
30033
+ ...input.provenance?.scope ? { scope: input.provenance.scope } : {},
30034
+ ...input.provenance?.parentEvidence ? { parentEvidence: input.provenance.parentEvidence } : {}
30035
+ }
29940
30036
  };
29941
30037
  }
30038
+ function verifyArtifact(artifact) {
30039
+ return sha256(artifact.content) === artifact.sha256;
30040
+ }
30041
+ function verifyEvidence(evidence) {
30042
+ const failed = [];
30043
+ for (const a of evidence.artifacts) {
30044
+ if (!verifyArtifact(a))
30045
+ failed.push(a.name);
30046
+ }
30047
+ return { ok: failed.length === 0, failed };
30048
+ }
30049
+
30050
+ // src/invariants.ts
30051
+ function checkFindingInvariants(finding) {
30052
+ const violations = [];
30053
+ if (finding.status === "confirmed" && finding.evidence.length === 0) {
30054
+ violations.push({ invariant: "confirmed-requires-evidence", detail: `finding ${finding.id} is confirmed with no evidence` });
30055
+ }
30056
+ if (finding.status === "confirmed" && finding.validation.negative_control === false) {
30057
+ violations.push({ invariant: "confirmed-requires-clean-negative-control", detail: `finding ${finding.id} confirmed with a failed negative control` });
30058
+ }
30059
+ if (finding.status === "confirmed" && finding.validation.scope_allowed === false) {
30060
+ violations.push({ invariant: "confirmed-requires-scope-allowed", detail: `finding ${finding.id} confirmed out of scope` });
30061
+ }
30062
+ for (const e of finding.evidence) {
30063
+ for (const a of e.artifacts) {
30064
+ if (hasSecrets(a.content)) {
30065
+ violations.push({ invariant: "no-secret-in-evidence", detail: `evidence ${e.evidence_id} artifact ${a.name} contains a raw secret` });
30066
+ }
30067
+ }
30068
+ }
30069
+ for (const e of finding.evidence) {
30070
+ const v = verifyEvidence(e);
30071
+ if (!v.ok) {
30072
+ violations.push({ invariant: "evidence-hash-stable", detail: `evidence ${e.evidence_id} artifacts ${v.failed.join(",")} failed integrity check` });
30073
+ }
30074
+ }
30075
+ return violations;
30076
+ }
30077
+ function assertFindingInvariants(finding) {
30078
+ const violations = checkFindingInvariants(finding);
30079
+ if (violations.length > 0) {
30080
+ throw new Error(`invariant violation: ${violations.map((v) => v.invariant).join(", ")}`);
30081
+ }
30082
+ }
29942
30083
 
29943
30084
  // src/finding.ts
29944
30085
  var LIFECYCLE_TRANSITIONS = {
@@ -30000,7 +30141,7 @@ function makeFinding(input) {
30000
30141
  const status = input.status ?? "detected";
30001
30142
  const factors = confidenceFactorsFor(input);
30002
30143
  const confidence = computeConfidence(factors);
30003
- return {
30144
+ const finding = {
30004
30145
  id: nextFindingId(),
30005
30146
  status,
30006
30147
  title: input.title,
@@ -30023,6 +30164,8 @@ function makeFinding(input) {
30023
30164
  remediation: {},
30024
30165
  timestamps: { created: now, updated: now }
30025
30166
  };
30167
+ recordAudit("finding_created", { tool: "eagle-eye", target: input.target.host ?? input.target.endpoint, result: status, correlationId: finding.id });
30168
+ return finding;
30026
30169
  }
30027
30170
  function confidenceFactorsFor(_input) {
30028
30171
  return {
@@ -30042,6 +30185,7 @@ function transition(finding, to) {
30042
30185
  return updated;
30043
30186
  }
30044
30187
  function attachEvidence(finding, evidence) {
30188
+ recordAudit("evidence_created", { tool: "blitzstrike", target: finding.target.host ?? finding.target.endpoint, result: evidence.evidence_id, correlationId: finding.id });
30045
30189
  return {
30046
30190
  ...finding,
30047
30191
  evidence: [...finding.evidence, evidence],
@@ -30049,8 +30193,12 @@ function attachEvidence(finding, evidence) {
30049
30193
  };
30050
30194
  }
30051
30195
  function confirmFinding(finding, opts) {
30196
+ if (opts.evidence.length === 0) {
30197
+ throw new Error("invariant: cannot confirm a finding without evidence");
30198
+ }
30052
30199
  const evidence = opts.evidence.map((e) => makeEvidence(e));
30053
30200
  const negativeControl = opts.negative_control ?? true;
30201
+ const scopeAllowed = opts.scope_allowed ?? true;
30054
30202
  const confidence = computeConfidence({
30055
30203
  static_analysis: true,
30056
30204
  data_flow: true,
@@ -30061,7 +30209,7 @@ function confirmFinding(finding, opts) {
30061
30209
  });
30062
30210
  const level = confidenceLevel(confidence);
30063
30211
  const status = negativeControl ? "confirmed" : "hypothesis";
30064
- return {
30212
+ const confirmed = {
30065
30213
  ...finding,
30066
30214
  status,
30067
30215
  confidence,
@@ -30070,11 +30218,15 @@ function confirmFinding(finding, opts) {
30070
30218
  performed: true,
30071
30219
  status: negativeControl ? "confirmed" : "unconfirmed",
30072
30220
  baseline: opts.baseline ?? true,
30073
- negative_control: negativeControl
30221
+ negative_control: negativeControl,
30222
+ scope_allowed: scopeAllowed
30074
30223
  },
30075
30224
  evidence: [...finding.evidence, ...evidence],
30076
30225
  timestamps: { ...finding.timestamps, updated: new Date().toISOString() }
30077
30226
  };
30227
+ assertFindingInvariants(confirmed);
30228
+ recordAudit("validation_finished", { tool: "strike", target: finding.target.host ?? finding.target.endpoint, result: status, correlationId: finding.id });
30229
+ return confirmed;
30078
30230
  }
30079
30231
  function rejectFinding(finding, status, reason, evidence) {
30080
30232
  const ev = evidence?.map((e) => makeEvidence(e)) ?? [];
@@ -30094,9 +30246,9 @@ function loadChains() {
30094
30246
  if (_chainsCache)
30095
30247
  return _chainsCache;
30096
30248
  try {
30097
- const dir = join4(fileURLToPath2(new URL(".", import.meta.url)), "..");
30098
- const path = join4(dir, "chains.json");
30099
- const raw = JSON.parse(readFileSync4(path, "utf8"));
30249
+ const dir = join6(fileURLToPath3(new URL(".", import.meta.url)), "..");
30250
+ const path = join6(dir, "chains.json");
30251
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
30100
30252
  _chainsCache = raw.chains ?? [];
30101
30253
  return _chainsCache;
30102
30254
  } catch {
@@ -30105,40 +30257,50 @@ function loadChains() {
30105
30257
  }
30106
30258
  }
30107
30259
  function scopeCheck(target, scope = "", mode = "bug-bounty") {
30260
+ let result;
30108
30261
  if (mode === "ctf" || mode === "reverse-engineering" || mode === "offensive") {
30109
- return { target, allowed: true, enforcement: "off", reason: `mode=${mode} disables scope enforcement` };
30110
- }
30111
- if (!scope.trim()) {
30112
- return { target, allowed: false, enforcement: "strict", reason: "no scope provided; refusing active testing" };
30113
- }
30114
- const inScope = [];
30115
- const outScope = [];
30116
- for (const raw of scope.split(/[,\n]/)) {
30117
- const line = raw.trim();
30118
- if (!line)
30119
- continue;
30120
- if (line.startsWith("-"))
30121
- outScope.push(line.slice(1).trim());
30122
- else
30123
- inScope.push(line);
30124
- }
30125
- let host = target;
30126
- host = host.replace(/^https?:\/\//, "");
30127
- host = host.split("/")[0].split(":")[0];
30128
- for (const exc of outScope) {
30129
- if (exc && (host.includes(exc) || host.endsWith("." + exc.replace(/^\./, "")))) {
30130
- return { target, allowed: false, enforcement: "strict", reason: `target '${host}' is explicitly out of scope (${exc})` };
30262
+ result = { target, allowed: true, enforcement: "off", reason: `mode=${mode} disables scope enforcement` };
30263
+ } else if (!scope.trim()) {
30264
+ result = { target, allowed: false, enforcement: "strict", reason: "no scope provided; refusing active testing" };
30265
+ } else {
30266
+ const inScope = [];
30267
+ const outScope = [];
30268
+ for (const raw of scope.split(/[,\n]/)) {
30269
+ const line = raw.trim();
30270
+ if (!line)
30271
+ continue;
30272
+ if (line.startsWith("-"))
30273
+ outScope.push(line.slice(1).trim());
30274
+ else
30275
+ inScope.push(line);
30276
+ }
30277
+ let host = target;
30278
+ host = host.replace(/^https?:\/\//, "");
30279
+ host = host.split("/")[0].split(":")[0];
30280
+ let decided = false;
30281
+ result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' not in scope` };
30282
+ for (const exc of outScope) {
30283
+ if (exc && (host.includes(exc) || host.endsWith("." + exc.replace(/^\./, "")))) {
30284
+ result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' is explicitly out of scope (${exc})` };
30285
+ decided = true;
30286
+ break;
30287
+ }
30131
30288
  }
30132
- }
30133
- for (const allowed of inScope) {
30134
- const a = allowed.replace(/\*/g, "");
30135
- if (!a)
30136
- continue;
30137
- if (host.includes(a) || host.endsWith("." + a.replace(/^\./, ""))) {
30138
- return { target, allowed: true, enforcement: "strict", reason: `matched in-scope pattern '${allowed}'` };
30289
+ if (!decided) {
30290
+ for (const allowed of inScope) {
30291
+ const a = allowed.replace(/\*/g, "");
30292
+ if (!a)
30293
+ continue;
30294
+ if (host.includes(a) || host.endsWith("." + a.replace(/^\./, ""))) {
30295
+ result = { target, allowed: true, enforcement: "strict", reason: `matched in-scope pattern '${allowed}'` };
30296
+ decided = true;
30297
+ break;
30298
+ }
30299
+ }
30139
30300
  }
30140
30301
  }
30141
- return { target, allowed: false, enforcement: "strict", reason: `target '${host}' not in scope` };
30302
+ recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
30303
+ return result;
30142
30304
  }
30143
30305
  function classifySink(sink) {
30144
30306
  if (/(move_uploaded_file|file_put_contents|fwrite)/.test(sink))
@@ -30202,7 +30364,7 @@ function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000,
30202
30364
  };
30203
30365
  const isPath = (() => {
30204
30366
  try {
30205
- return existsSync3(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
30367
+ return existsSync4(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
30206
30368
  } catch {
30207
30369
  return false;
30208
30370
  }
@@ -30319,18 +30481,18 @@ function listChains() {
30319
30481
  }
30320
30482
 
30321
30483
  // src/catalog.ts
30322
- import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
30323
- import { join as join5 } from "node:path";
30324
- import { fileURLToPath as fileURLToPath3 } from "node:url";
30484
+ import { readFileSync as readFileSync7, readdirSync as readdirSync2 } from "node:fs";
30485
+ import { join as join7 } from "node:path";
30486
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
30325
30487
  import { spawnSync } from "node:child_process";
30326
- var ROOT2 = join5(fileURLToPath3(new URL(".", import.meta.url)), "..");
30488
+ var ROOT2 = join7(fileURLToPath4(new URL(".", import.meta.url)), "..");
30327
30489
  var _toolsCache = null;
30328
30490
  var _skillsCache = null;
30329
30491
  function loadTools() {
30330
30492
  if (_toolsCache)
30331
30493
  return _toolsCache;
30332
30494
  try {
30333
- const raw = JSON.parse(readFileSync5(join5(ROOT2, "tools-catalog.json"), "utf8"));
30495
+ const raw = JSON.parse(readFileSync7(join7(ROOT2, "tools-catalog.json"), "utf8"));
30334
30496
  _toolsCache = raw.tools ?? [];
30335
30497
  } catch {
30336
30498
  _toolsCache = [];
@@ -30340,14 +30502,14 @@ function loadTools() {
30340
30502
  function loadSkills() {
30341
30503
  if (_skillsCache)
30342
30504
  return _skillsCache;
30343
- const dir = join5(ROOT2, "skills");
30505
+ const dir = join7(ROOT2, "skills");
30344
30506
  const out = [];
30345
30507
  try {
30346
30508
  for (const f of readdirSync2(dir)) {
30347
30509
  if (!f.endsWith(".md"))
30348
30510
  continue;
30349
- const p = join5(dir, f);
30350
- const text = readFileSync5(p, "utf8");
30511
+ const p = join7(dir, f);
30512
+ const text = readFileSync7(p, "utf8");
30351
30513
  const descMatch = text.match(/^description:\s*(.+)$/m);
30352
30514
  out.push({
30353
30515
  name: f.replace(/\.md$/, ""),
@@ -30420,7 +30582,7 @@ function readSkill(name) {
30420
30582
  if (!s)
30421
30583
  return { found: false, query: name };
30422
30584
  try {
30423
- const text = readFileSync5(join5(ROOT2, "skills", s.file), "utf8");
30585
+ const text = readFileSync7(join7(ROOT2, "skills", s.file), "utf8");
30424
30586
  return { found: true, name: s.name, content: text.slice(0, 12000) };
30425
30587
  } catch {
30426
30588
  return { found: false, query: name, error: "unreadable" };
@@ -30453,19 +30615,19 @@ function ensureTool(name) {
30453
30615
  }
30454
30616
 
30455
30617
  // src/intel.ts
30456
- import { readFileSync as readFileSync6, existsSync as existsSync4 } from "node:fs";
30457
- import { join as join6 } from "node:path";
30458
- import { fileURLToPath as fileURLToPath4 } from "node:url";
30618
+ import { readFileSync as readFileSync8, existsSync as existsSync5 } from "node:fs";
30619
+ import { join as join8 } from "node:path";
30620
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
30459
30621
  import { readdirSync as readdirSync3 } from "node:fs";
30460
- import { homedir as homedir2 } from "node:os";
30461
- var ROOT3 = join6(fileURLToPath4(new URL(".", import.meta.url)), "..");
30462
- var INTEL = join6(ROOT3, "intelligence");
30622
+ import { homedir as homedir3 } from "node:os";
30623
+ var ROOT3 = join8(fileURLToPath5(new URL(".", import.meta.url)), "..");
30624
+ var INTEL = join8(ROOT3, "intelligence");
30463
30625
  function loadJson(name) {
30464
- const p = join6(INTEL, `${name}.json`);
30465
- if (!existsSync4(p))
30626
+ const p = join8(INTEL, `${name}.json`);
30627
+ if (!existsSync5(p))
30466
30628
  return null;
30467
30629
  try {
30468
- return JSON.parse(readFileSync6(p, "utf8"));
30630
+ return JSON.parse(readFileSync8(p, "utf8"));
30469
30631
  } catch {
30470
30632
  return null;
30471
30633
  }
@@ -30597,18 +30759,18 @@ function attackVectors(category) {
30597
30759
  }
30598
30760
  return { found: true, id: found.id, title: found.title, count: found.vectors.length, vectors: found.vectors };
30599
30761
  }
30600
- var PACKAGE_PAYLOADS = join6(ROOT3, "payloads");
30601
- var PACKAGE_TEMPLATES = join6(ROOT3, "templates");
30602
- var DATA_ROOT = process.env.BLITZSTRIKE_DATA ?? join6(homedir2(), ".blitzstrike", "data");
30762
+ var PACKAGE_PAYLOADS = join8(ROOT3, "payloads");
30763
+ var PACKAGE_TEMPLATES = join8(ROOT3, "templates");
30764
+ var DATA_ROOT = process.env.BLITZSTRIKE_DATA ?? join8(homedir3(), ".blitzstrike", "data");
30603
30765
  function payloadsDir() {
30604
- if (existsSync4(PACKAGE_PAYLOADS))
30766
+ if (existsSync5(PACKAGE_PAYLOADS))
30605
30767
  return PACKAGE_PAYLOADS;
30606
- return join6(DATA_ROOT, "payloads");
30768
+ return join8(DATA_ROOT, "payloads");
30607
30769
  }
30608
30770
  function templatesDir() {
30609
- if (existsSync4(PACKAGE_TEMPLATES))
30771
+ if (existsSync5(PACKAGE_TEMPLATES))
30610
30772
  return PACKAGE_TEMPLATES;
30611
- return join6(DATA_ROOT, "templates");
30773
+ return join8(DATA_ROOT, "templates");
30612
30774
  }
30613
30775
  var PAYLOADS = payloadsDir();
30614
30776
  var TEMPLATES = templatesDir();
@@ -30644,7 +30806,7 @@ function payloadLookup(topic) {
30644
30806
  const normalized = ALIASES[key] ?? key;
30645
30807
  const exact = cats.find((c) => c.toLowerCase() === normalized);
30646
30808
  if (exact) {
30647
- return { found: true, category: exact, files: listFiles(join6(PAYLOADS, exact)) };
30809
+ return { found: true, category: exact, files: listFiles(join8(PAYLOADS, exact)) };
30648
30810
  }
30649
30811
  const keyTokens = normalized.split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
30650
30812
  const matches = cats.filter((c) => {
@@ -30654,7 +30816,7 @@ function payloadLookup(topic) {
30654
30816
  return keyTokens.some((tok) => new RegExp(`(^|[^a-z0-9])${tok}`).test(cl));
30655
30817
  });
30656
30818
  if (matches.length > 0) {
30657
- return { found: true, category: matches[0], files: listFiles(join6(PAYLOADS, matches[0])), alternatives: matches.slice(0, 10) };
30819
+ return { found: true, category: matches[0], files: listFiles(join8(PAYLOADS, matches[0])), alternatives: matches.slice(0, 10) };
30658
30820
  }
30659
30821
  return { found: false, topic, suggestions: cats.slice(0, 20) };
30660
30822
  }
@@ -30663,19 +30825,19 @@ function readPayload(category, file) {
30663
30825
  const exact = cats.find((c) => c.toLowerCase() === category.toLowerCase());
30664
30826
  if (!exact)
30665
30827
  return { found: false, category, suggestions: cats.slice(0, 20) };
30666
- const dir = join6(PAYLOADS, exact);
30828
+ const dir = join8(PAYLOADS, exact);
30667
30829
  const files = listFiles(dir);
30668
30830
  if (!file) {
30669
30831
  const readme = files.find((f) => f.toLowerCase() === "readme.md");
30670
30832
  if (readme) {
30671
- return { found: true, category: exact, file: readme, content: readFileSync6(join6(dir, readme), "utf8").slice(0, 20000) };
30833
+ return { found: true, category: exact, file: readme, content: readFileSync8(join8(dir, readme), "utf8").slice(0, 20000) };
30672
30834
  }
30673
30835
  return { found: true, category: exact, files };
30674
30836
  }
30675
30837
  const f = files.find((x) => x.toLowerCase() === file.toLowerCase());
30676
30838
  if (!f)
30677
30839
  return { found: false, category: exact, file, suggestions: files };
30678
- return { found: true, category: exact, file: f, content: readFileSync6(join6(dir, f), "utf8").slice(0, 20000) };
30840
+ return { found: true, category: exact, file: f, content: readFileSync8(join8(dir, f), "utf8").slice(0, 20000) };
30679
30841
  }
30680
30842
  function listFiles(dir) {
30681
30843
  try {
@@ -30690,7 +30852,7 @@ function countTemplates() {
30690
30852
  const walk = (dir) => {
30691
30853
  for (const e of readdirSync3(dir, { withFileTypes: true })) {
30692
30854
  if (e.isDirectory())
30693
- walk(join6(dir, e.name));
30855
+ walk(join8(dir, e.name));
30694
30856
  else if (e.name.endsWith(".yaml"))
30695
30857
  n++;
30696
30858
  }
@@ -30715,14 +30877,14 @@ function templateLookup(topic, limit = 10) {
30715
30877
  }
30716
30878
  for (const e of entries) {
30717
30879
  if (e.isDirectory())
30718
- walk(join6(dir, e.name), depth + 1);
30880
+ walk(join8(dir, e.name), depth + 1);
30719
30881
  else if (e.name.endsWith(".yaml")) {
30720
30882
  const name = e.name.toLowerCase();
30721
30883
  if (name.includes(key)) {
30722
- const full = join6(dir, e.name);
30884
+ const full = join8(dir, e.name);
30723
30885
  let id = "";
30724
30886
  try {
30725
- const content = readFileSync6(full, "utf8");
30887
+ const content = readFileSync8(full, "utf8");
30726
30888
  const m = content.match(/^\s*id:\s*(.+)$/m);
30727
30889
  if (m)
30728
30890
  id = m[1].trim();
@@ -30882,7 +31044,7 @@ async function activeScan(target, scope = "", mode = "bug-bounty", allowed = und
30882
31044
 
30883
31045
  // src/dataflow.ts
30884
31046
  init_scanner();
30885
- import { readFileSync as readFileSync7 } from "node:fs";
31047
+ import { readFileSync as readFileSync9 } from "node:fs";
30886
31048
  var SOURCES = {
30887
31049
  $_GET: { id: "http_get", label: "HTTP GET parameter", attacker_controlled: true },
30888
31050
  $_POST: { id: "http_post", label: "HTTP POST body", attacker_controlled: true },
@@ -30964,7 +31126,7 @@ function traceDataFlow(path) {
30964
31126
  return result;
30965
31127
  let text;
30966
31128
  try {
30967
- text = readFileSync7(path, "utf8");
31129
+ text = readFileSync9(path, "utf8");
30968
31130
  } catch {
30969
31131
  return result;
30970
31132
  }
@@ -31043,7 +31205,7 @@ function groupVariants(results) {
31043
31205
  // src/taint.ts
31044
31206
  init_scanner();
31045
31207
  var import_php_parser = __toESM(require_src(), 1);
31046
- import { readFileSync as readFileSync8 } from "node:fs";
31208
+ import { readFileSync as readFileSync10 } from "node:fs";
31047
31209
  var parser = new import_php_parser.Engine({
31048
31210
  parser: { extractDoc: false, php7: true },
31049
31211
  ast: { withPositions: true, withSource: false }
@@ -31341,7 +31503,7 @@ function analyzeTaint(path) {
31341
31503
  const result = { file: path, findings: [], suppressed: 0 };
31342
31504
  let code;
31343
31505
  try {
31344
- code = readFileSync8(path, "utf8");
31506
+ code = readFileSync10(path, "utf8");
31345
31507
  } catch {
31346
31508
  return result;
31347
31509
  }
@@ -50615,16 +50777,16 @@ function reportJson(findings, opts = {}) {
50615
50777
  }
50616
50778
 
50617
50779
  // src/benchmark.ts
50618
- import { readFileSync as readFileSync9, readdirSync as readdirSync4, writeFileSync as writeFileSync2, rmSync } from "node:fs";
50619
- import { join as join7 } from "node:path";
50620
- import { fileURLToPath as fileURLToPath5 } from "node:url";
50780
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, writeFileSync as writeFileSync2, rmSync } from "node:fs";
50781
+ import { join as join9 } from "node:path";
50782
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
50621
50783
  import { tmpdir } from "node:os";
50622
- var ROOT4 = join7(fileURLToPath5(new URL(".", import.meta.url)), "..");
50623
- var CORPUS = join7(ROOT4, "bench", "corpus");
50784
+ var ROOT4 = join9(fileURLToPath6(new URL(".", import.meta.url)), "..");
50785
+ var CORPUS = join9(ROOT4, "bench", "corpus");
50624
50786
  function parseCorpusFile(path) {
50625
50787
  let text;
50626
50788
  try {
50627
- text = readFileSync9(path, "utf8");
50789
+ text = readFileSync11(path, "utf8");
50628
50790
  } catch {
50629
50791
  return null;
50630
50792
  }
@@ -50674,7 +50836,7 @@ function loadCorpus() {
50674
50836
  for (const f of readdirSync4(CORPUS)) {
50675
50837
  if (!f.endsWith(".md"))
50676
50838
  continue;
50677
- const e = parseCorpusFile(join7(CORPUS, f));
50839
+ const e = parseCorpusFile(join9(CORPUS, f));
50678
50840
  if (e)
50679
50841
  entries.push(e);
50680
50842
  }
@@ -50686,7 +50848,7 @@ function loadCorpus() {
50686
50848
  function detect(entry) {
50687
50849
  const lang = entry.language;
50688
50850
  if (lang === "php") {
50689
- const tmp = join7(tmpdir(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
50851
+ const tmp = join9(tmpdir(), `blitzstrike-bench-${entry.id}-${process.pid}.php`);
50690
50852
  writeFileSync2(tmp, entry.code);
50691
50853
  const r = analyzeTaint(tmp);
50692
50854
  rmSync(tmp, { force: true });
@@ -50833,6 +50995,7 @@ function coverageMatrix() {
50833
50995
  }
50834
50996
 
50835
50997
  // src/server.ts
50998
+ init_update();
50836
50999
  function createServer() {
50837
51000
  const server = new McpServer({
50838
51001
  name: "blitzstrike",
@@ -51589,7 +51752,7 @@ function createServer() {
51589
51752
  } catch {
51590
51753
  return { content: [{ type: "text", text: JSON.stringify({ error: "invalid JSON" }) }] };
51591
51754
  }
51592
- const opts = { title, scope, version: "1.0.23" };
51755
+ const opts = { title, scope, version: VERSION };
51593
51756
  const out = (format ?? "markdown") === "json" ? reportJson(list, opts) : reportMarkdown(list, opts);
51594
51757
  return { content: [{ type: "text", text: out }] };
51595
51758
  });
@@ -51607,6 +51770,14 @@ function createServer() {
51607
51770
  }, async () => {
51608
51771
  return { content: [{ type: "text", text: JSON.stringify(coverageMatrix()) }] };
51609
51772
  });
51773
+ server.registerTool("check_update", {
51774
+ title: "Check for a newer blitzstrike version",
51775
+ description: "UPDATE: query the npm registry for the latest blitzstrike version. Tells a driving agent whether the package or the data cache is stale.",
51776
+ inputSchema: {}
51777
+ }, async () => {
51778
+ const latest = await checkLatestVersion();
51779
+ return { content: [{ type: "text", text: JSON.stringify({ latest, note: "package updates via `npx blitzstrike` (always-latest) or `npm i -g blitzstrike@latest`; heavy data via `blitzstrike sync-data`" }) }] };
51780
+ });
51610
51781
  return server;
51611
51782
  }
51612
51783
  async function serve() {
@@ -51616,14 +51787,14 @@ async function serve() {
51616
51787
  }
51617
51788
 
51618
51789
  // src/cli.ts
51619
- import { readFileSync as readFileSync10, existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "node:fs";
51620
- import { join as join8, dirname } from "node:path";
51621
- import { homedir as homedir3 } from "node:os";
51622
- import { fileURLToPath as fileURLToPath6 } from "node:url";
51623
- import { spawnSync as spawnSync2 } from "node:child_process";
51624
- var ROOT5 = join8(fileURLToPath6(new URL(".", import.meta.url)), "..");
51790
+ import { readFileSync as readFileSync13, existsSync as existsSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "node:fs";
51791
+ import { join as join11, dirname as dirname2 } from "node:path";
51792
+ import { homedir as homedir5 } from "node:os";
51793
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
51794
+ import { spawnSync as spawnSync3 } from "node:child_process";
51795
+ var ROOT6 = join11(fileURLToPath8(new URL(".", import.meta.url)), "..");
51625
51796
  function which(cmd) {
51626
- return spawnSync2(`command -v ${cmd}`, { shell: true }).status === 0;
51797
+ return spawnSync3(`command -v ${cmd}`, { shell: true }).status === 0;
51627
51798
  }
51628
51799
  function installedCount() {
51629
51800
  const tools = loadTools();
@@ -51656,7 +51827,7 @@ function runDoctor() {
51656
51827
  detail: fofa ? "set" : "FOFA_EMAIL/FOFA_KEY not set",
51657
51828
  fix: fofa ? undefined : "export FOFA_EMAIL=... && export FOFA_KEY=... (enables fofa_search)"
51658
51829
  });
51659
- const dataOk = existsSync5(join8(ROOT5, "chains.json")) && existsSync5(join8(ROOT5, "tools-catalog.json"));
51830
+ const dataOk = existsSync6(join11(ROOT6, "chains.json")) && existsSync6(join11(ROOT6, "tools-catalog.json"));
51660
51831
  issues.push({
51661
51832
  name: "Data layers (chains + tools-catalog)",
51662
51833
  status: dataOk ? "ok" : "fail",
@@ -51684,73 +51855,73 @@ BlitzStrike — ${verdict}`);
51684
51855
  function resolveCommand() {
51685
51856
  if (which("blitzstrike"))
51686
51857
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
51687
- const src = join8(ROOT5, "src", "index.ts");
51688
- if (which("bun") && existsSync5(src))
51858
+ const src = join11(ROOT6, "src", "index.ts");
51859
+ if (which("bun") && existsSync6(src))
51689
51860
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
51690
51861
  if (which("npx"))
51691
51862
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
51692
51863
  if (which("bunx"))
51693
51864
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
51694
- const dist = join8(ROOT5, "dist", "index.js");
51695
- if (existsSync5(dist))
51865
+ const dist = join11(ROOT6, "dist", "index.js");
51866
+ if (existsSync6(dist))
51696
51867
  return { command: "bun", args: [dist, "serve", "--mcp"] };
51697
51868
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
51698
51869
  }
51699
51870
  function readJson(p) {
51700
51871
  try {
51701
- return JSON.parse(readFileSync10(p, "utf8"));
51872
+ return JSON.parse(readFileSync13(p, "utf8"));
51702
51873
  } catch {
51703
51874
  return null;
51704
51875
  }
51705
51876
  }
51706
51877
  function jsonMcpServersWrite(p) {
51707
51878
  return (command, args) => {
51708
- const dir = dirname(p);
51709
- if (!existsSync5(dir))
51710
- mkdirSync2(dir, { recursive: true });
51879
+ const dir = dirname2(p);
51880
+ if (!existsSync6(dir))
51881
+ mkdirSync4(dir, { recursive: true });
51711
51882
  const existing = readJson(p) ?? {};
51712
51883
  existing.mcpServers = {
51713
51884
  ...existing.mcpServers ?? {},
51714
51885
  blitzstrike: { command, args }
51715
51886
  };
51716
- writeFileSync3(p, JSON.stringify(existing, null, 2));
51887
+ writeFileSync4(p, JSON.stringify(existing, null, 2));
51717
51888
  };
51718
51889
  }
51719
51890
  function opencodeWrite(p) {
51720
51891
  return (_command, _args) => {
51721
- const dir = dirname(p);
51722
- if (!existsSync5(dir))
51723
- mkdirSync2(dir, { recursive: true });
51892
+ const dir = dirname2(p);
51893
+ if (!existsSync6(dir))
51894
+ mkdirSync4(dir, { recursive: true });
51724
51895
  const existing = readJson(p) ?? {};
51725
51896
  existing.mcp = {
51726
51897
  ...existing.mcp ?? {},
51727
51898
  blitzstrike: { type: "local", command: ["npx", "-y", "blitzstrike", "serve", "--mcp"], enabled: true }
51728
51899
  };
51729
- writeFileSync3(p, JSON.stringify(existing, null, 2));
51900
+ writeFileSync4(p, JSON.stringify(existing, null, 2));
51730
51901
  copyOpenCodeAgents();
51731
51902
  };
51732
51903
  }
51733
51904
  function copyOpenCodeAgents() {
51734
- const srcDir = join8(ROOT5, "opencode-agents");
51735
- const dstDir = join8(homedir3(), ".config", "opencode", "agents");
51736
- if (!existsSync5(srcDir))
51905
+ const srcDir = join11(ROOT6, "opencode-agents");
51906
+ const dstDir = join11(homedir5(), ".config", "opencode", "agents");
51907
+ if (!existsSync6(srcDir))
51737
51908
  return;
51738
- if (!existsSync5(dstDir))
51739
- mkdirSync2(dstDir, { recursive: true });
51909
+ if (!existsSync6(dstDir))
51910
+ mkdirSync4(dstDir, { recursive: true });
51740
51911
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
51741
- const src = join8(srcDir, f);
51742
- const dst = join8(dstDir, f);
51743
- if (existsSync5(src)) {
51744
- writeFileSync3(dst, readFileSync10(src, "utf8"));
51912
+ const src = join11(srcDir, f);
51913
+ const dst = join11(dstDir, f);
51914
+ if (existsSync6(src)) {
51915
+ writeFileSync4(dst, readFileSync13(src, "utf8"));
51745
51916
  }
51746
51917
  }
51747
51918
  }
51748
51919
  function codexWrite(p) {
51749
51920
  return (command, args) => {
51750
- const dir = dirname(p);
51751
- if (!existsSync5(dir))
51752
- mkdirSync2(dir, { recursive: true });
51753
- let existing = existsSync5(p) ? readFileSync10(p, "utf8") : "";
51921
+ const dir = dirname2(p);
51922
+ if (!existsSync6(dir))
51923
+ mkdirSync4(dir, { recursive: true });
51924
+ let existing = existsSync6(p) ? readFileSync13(p, "utf8") : "";
51754
51925
  if (!existing.trimEnd().endsWith(`
51755
51926
  `))
51756
51927
  existing += `
@@ -51761,15 +51932,15 @@ function codexWrite(p) {
51761
51932
  command = "${command}"
51762
51933
  args = ${JSON.stringify(args)}
51763
51934
  `;
51764
- writeFileSync3(p, existing);
51935
+ writeFileSync4(p, existing);
51765
51936
  };
51766
51937
  }
51767
51938
  function hermesWrite(p) {
51768
51939
  return (command, args) => {
51769
- const dir = dirname(p);
51770
- if (!existsSync5(dir))
51771
- mkdirSync2(dir, { recursive: true });
51772
- let existing = existsSync5(p) ? readFileSync10(p, "utf8") : "";
51940
+ const dir = dirname2(p);
51941
+ if (!existsSync6(dir))
51942
+ mkdirSync4(dir, { recursive: true });
51943
+ let existing = existsSync6(p) ? readFileSync13(p, "utf8") : "";
51773
51944
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
51774
51945
  if (!existing.trimEnd().endsWith(`
51775
51946
  `))
@@ -51790,80 +51961,80 @@ mcp_servers:
51790
51961
  args: ${JSON.stringify(args)}
51791
51962
  `;
51792
51963
  }
51793
- writeFileSync3(p, existing);
51964
+ writeFileSync4(p, existing);
51794
51965
  };
51795
51966
  }
51796
51967
  function detectAgents() {
51797
- const home = homedir3();
51968
+ const home = homedir5();
51798
51969
  const agents = [];
51799
- const claudeJson = join8(home, ".claude.json");
51970
+ const claudeJson = join11(home, ".claude.json");
51800
51971
  agents.push({
51801
51972
  name: "Claude Code",
51802
51973
  detectPath: claudeJson,
51803
- installed: () => existsSync5(claudeJson) || which("claude"),
51974
+ installed: () => existsSync6(claudeJson) || which("claude"),
51804
51975
  write: jsonMcpServersWrite(claudeJson)
51805
51976
  });
51806
- const claudeDesktop = join8(home, ".config", "Claude", "claude_desktop_config.json");
51977
+ const claudeDesktop = join11(home, ".config", "Claude", "claude_desktop_config.json");
51807
51978
  agents.push({
51808
51979
  name: "Claude Desktop",
51809
51980
  detectPath: claudeDesktop,
51810
- installed: () => existsSync5(claudeDesktop),
51981
+ installed: () => existsSync6(claudeDesktop),
51811
51982
  write: jsonMcpServersWrite(claudeDesktop)
51812
51983
  });
51813
- const cursor = join8(home, ".cursor", "mcp.json");
51984
+ const cursor = join11(home, ".cursor", "mcp.json");
51814
51985
  agents.push({
51815
51986
  name: "Cursor",
51816
51987
  detectPath: cursor,
51817
- installed: () => existsSync5(join8(home, ".cursor")) || which("cursor"),
51988
+ installed: () => existsSync6(join11(home, ".cursor")) || which("cursor"),
51818
51989
  write: jsonMcpServersWrite(cursor)
51819
51990
  });
51820
- const opencode = join8(home, ".config", "opencode", "opencode.jsonc");
51991
+ const opencode = join11(home, ".config", "opencode", "opencode.jsonc");
51821
51992
  agents.push({
51822
51993
  name: "OpenCode",
51823
51994
  detectPath: opencode,
51824
- installed: () => existsSync5(join8(home, ".config", "opencode")) || which("opencode"),
51995
+ installed: () => existsSync6(join11(home, ".config", "opencode")) || which("opencode"),
51825
51996
  write: opencodeWrite(opencode)
51826
51997
  });
51827
- const codex = join8(home, ".codex", "config.toml");
51998
+ const codex = join11(home, ".codex", "config.toml");
51828
51999
  agents.push({
51829
52000
  name: "Codex",
51830
52001
  detectPath: codex,
51831
- installed: () => existsSync5(join8(home, ".codex")) || which("codex"),
52002
+ installed: () => existsSync6(join11(home, ".codex")) || which("codex"),
51832
52003
  write: codexWrite(codex)
51833
52004
  });
51834
- const hermes = join8(home, ".hermes", "config.yaml");
52005
+ const hermes = join11(home, ".hermes", "config.yaml");
51835
52006
  agents.push({
51836
52007
  name: "Hermes",
51837
52008
  detectPath: hermes,
51838
- installed: () => existsSync5(join8(home, ".hermes")) || which("hermes"),
52009
+ installed: () => existsSync6(join11(home, ".hermes")) || which("hermes"),
51839
52010
  write: hermesWrite(hermes)
51840
52011
  });
51841
- const gemini = join8(home, ".gemini", "settings.json");
52012
+ const gemini = join11(home, ".gemini", "settings.json");
51842
52013
  agents.push({
51843
52014
  name: "Gemini CLI",
51844
52015
  detectPath: gemini,
51845
- installed: () => existsSync5(join8(home, ".gemini")) || which("gemini"),
52016
+ installed: () => existsSync6(join11(home, ".gemini")) || which("gemini"),
51846
52017
  write: jsonMcpServersWrite(gemini)
51847
52018
  });
51848
- const windsurf = join8(home, ".codeium", "windsurf", "mcp_config.json");
52019
+ const windsurf = join11(home, ".codeium", "windsurf", "mcp_config.json");
51849
52020
  agents.push({
51850
52021
  name: "Windsurf",
51851
52022
  detectPath: windsurf,
51852
- installed: () => existsSync5(join8(home, ".codeium")) || which("windsurf"),
52023
+ installed: () => existsSync6(join11(home, ".codeium")) || which("windsurf"),
51853
52024
  write: jsonMcpServersWrite(windsurf)
51854
52025
  });
51855
- const copilot = join8(home, ".copilot", "mcp.json");
52026
+ const copilot = join11(home, ".copilot", "mcp.json");
51856
52027
  agents.push({
51857
52028
  name: "Copilot",
51858
52029
  detectPath: copilot,
51859
- installed: () => existsSync5(join8(home, ".copilot")),
52030
+ installed: () => existsSync6(join11(home, ".copilot")),
51860
52031
  write: jsonMcpServersWrite(copilot)
51861
52032
  });
51862
- const cline = join8(home, ".cline", "mcp_settings.json");
52033
+ const cline = join11(home, ".cline", "mcp_settings.json");
51863
52034
  agents.push({
51864
52035
  name: "Cline",
51865
52036
  detectPath: cline,
51866
- installed: () => existsSync5(join8(home, ".cline")),
52037
+ installed: () => existsSync6(join11(home, ".cline")),
51867
52038
  write: jsonMcpServersWrite(cline)
51868
52039
  });
51869
52040
  return agents;
@@ -51907,29 +52078,31 @@ Registered with ${ok}/${installed.length} agent(s).`);
51907
52078
  }
51908
52079
 
51909
52080
  // src/index.ts
51910
- import { readFileSync as readFileSync12, existsSync as existsSync6 } from "node:fs";
51911
- import { join as join10 } from "node:path";
51912
- import { fileURLToPath as fileURLToPath8 } from "node:url";
51913
- var ROOT7 = join10(fileURLToPath8(new URL(".", import.meta.url)), "..");
52081
+ import { readFileSync as readFileSync14, existsSync as existsSync7 } from "node:fs";
52082
+ import { join as join12 } from "node:path";
52083
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
52084
+ var ROOT7 = join12(fileURLToPath9(new URL(".", import.meta.url)), "..");
51914
52085
  function readVersion() {
51915
52086
  try {
51916
- const p = join10(ROOT7, "package.json");
51917
- if (existsSync6(p))
51918
- return JSON.parse(readFileSync12(p, "utf8")).version ?? "1.0.0";
52087
+ const p = join12(ROOT7, "package.json");
52088
+ if (existsSync7(p))
52089
+ return JSON.parse(readFileSync14(p, "utf8")).version ?? "1.0.0";
51919
52090
  } catch {}
51920
52091
  return "1.0.0";
51921
52092
  }
51922
- var VERSION = readVersion();
52093
+ var VERSION2 = readVersion();
51923
52094
  function usage() {
51924
- return `BlitzStrike ${VERSION} — MCP security-audit toolbelt
52095
+ return `BlitzStrike ${VERSION2} — MCP security-audit toolbelt
51925
52096
 
51926
52097
  Usage:
51927
52098
  blitzstrike serve --mcp Start the MCP server over stdio (default transport)
51928
52099
  blitzstrike doctor Environment health check (runtime + tools + creds)
51929
52100
  blitzstrike install Write MCP client config to detected clients
51930
52101
  blitzstrike install --dry-run Print the config without writing
51931
- blitzstrike version Print version
51932
- blitzstrike --help This help`;
52102
+ blitzstrike sync-data Fetch heavy datasets (payloads + templates) on-demand
52103
+ blitzstrike update Check for a newer version + refresh the data cache
52104
+ blitzstrike version Print version
52105
+ blitzstrike --help This help`;
51933
52106
  }
51934
52107
  async function main() {
51935
52108
  const args = process.argv.slice(2);
@@ -51938,7 +52111,7 @@ async function main() {
51938
52111
  return;
51939
52112
  }
51940
52113
  if (args[0] === "version" || args[0] === "--version" || args[0] === "-v") {
51941
- console.log(`blitzstrike ${VERSION}`);
52114
+ console.log(`blitzstrike ${VERSION2}`);
51942
52115
  return;
51943
52116
  }
51944
52117
  if (args[0] === "doctor") {
@@ -51958,6 +52131,22 @@ async function main() {
51958
52131
  console.log(` templates: ${r.templates.files} files ${r.templates.synced ? "(synced)" : "(already present)"}`);
51959
52132
  return;
51960
52133
  }
52134
+ if (args[0] === "update") {
52135
+ await Promise.resolve().then(() => init_update());
52136
+ const r = await runUpdate(VERSION2);
52137
+ console.log("Blitz Strike self-update");
52138
+ console.log(` current: ${r.current}`);
52139
+ console.log(` latest: ${r.latest ?? "unknown (registry unreachable)"}`);
52140
+ if (r.outdated) {
52141
+ console.log(` status: OUTDATED — update with: ${r.update_command}`);
52142
+ } else if (r.latest === null) {
52143
+ console.log(` status: could not reach npm registry`);
52144
+ } else {
52145
+ console.log(` status: up to date`);
52146
+ }
52147
+ console.log(` data: payloads ${r.data_sync.payloads.files} files, templates ${r.data_sync.templates.files} files ${r.data_sync.payloads.synced ? "(refreshed)" : ""}`);
52148
+ return;
52149
+ }
51961
52150
  if (args[0] === "serve") {
51962
52151
  await serve();
51963
52152
  return;
package/docs/findings.md CHANGED
@@ -76,12 +76,33 @@ validation + evidence.
76
76
 
77
77
  `src/evidence.ts`:
78
78
 
79
- - **Schema** — `evidence_id`, `type`, `description`, `source`, `sink`, `artifacts[]`, `recorded`.
79
+ - **Schema** — `evidence_id`, `type`, `description`, `source`, `sink`, `artifacts[]`, `recorded`, `provenance`.
80
+ - **Provenance (§16)** — every record carries chain-of-custody metadata: `tool`,
81
+ `version`, `target`, `scope`, `parentEvidence`.
80
82
  - **Integrity** — every artifact carries a SHA-256 (`sha256()`), verified by `verifyEvidence()`.
81
83
  - **Secret redaction** — `redactSecrets()` strips passwords, API keys, tokens,
82
84
  cookies, `Authorization` headers, private keys before persistence/report/log/context.
83
85
  - **Append-only** — confirmed evidence is never silently rewritten.
84
86
 
87
+ ## Finding invariants (§53)
88
+
89
+ `src/invariants.ts` enforces hard, machine-checkable rules:
90
+
91
+ | Invariant | Enforcement |
92
+ | :--- | :--- |
93
+ | CONFIRMED requires evidence | `confirmFinding` throws without evidence |
94
+ | CONFIRMED requires clean negative control | `checkFindingInvariants` |
95
+ | CONFIRMED requires in-scope target | `checkFindingInvariants` |
96
+ | No raw secret in persisted evidence | `checkFindingInvariants` |
97
+ | Evidence hash must stay stable | `checkFindingInvariants` |
98
+
99
+ ## Audit log (§45)
100
+
101
+ `src/audit.ts` emits structured append-only audit events to
102
+ `~/.blitzstrike/audit.jsonl`: `scope_checked`, `finding_created`,
103
+ `finding_updated`, `validation_finished`, `evidence_created`, … each carrying
104
+ timestamp, actor, tool, target, result, and correlationId.
105
+
85
106
  ## MCP tools
86
107
 
87
108
  | Tool | Purpose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
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": {