blitzstrike 1.0.10 → 1.0.12

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
@@ -22225,6 +22225,159 @@ function manualForTool(toolName) {
22225
22225
  return null;
22226
22226
  }
22227
22227
 
22228
+ // src/evidence.ts
22229
+ import { createHash as createHash2 } from "node:crypto";
22230
+ var REDACTION_PATTERNS = [
22231
+ [/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, "$1$2[REDACTED]"],
22232
+ [/(api[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
22233
+ [/(access[_-]?token\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
22234
+ [/(secret\s*[:=]\s*)["']?[A-Za-z0-9._/-]{16,}["']?/gi, "$1[REDACTED]"],
22235
+ [/(password\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
22236
+ [/(passwd\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
22237
+ [/(private[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9+/=_-]{16,}["']?/gi, "$1[REDACTED]"],
22238
+ [/(session\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
22239
+ [/(cookie\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
22240
+ [/(set-cookie\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]"],
22241
+ [/AKIA[0-9A-Z]{16}/g, "[REDACTED]"],
22242
+ [/ghp_[A-Za-z0-9]{20,}/g, "[REDACTED]"],
22243
+ [/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
22244
+ [/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]"],
22245
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
22246
+ ];
22247
+ function redactSecrets(text) {
22248
+ let out = text;
22249
+ for (const [re, repl] of REDACTION_PATTERNS) {
22250
+ out = out.replace(re, repl);
22251
+ }
22252
+ return out;
22253
+ }
22254
+ function redactObject(value) {
22255
+ if (typeof value === "string")
22256
+ return redactSecrets(value);
22257
+ if (Array.isArray(value))
22258
+ return value.map((v) => redactObject(v));
22259
+ if (value && typeof value === "object") {
22260
+ const out = {};
22261
+ for (const [k, v] of Object.entries(value)) {
22262
+ out[k] = redactObject(v);
22263
+ }
22264
+ return out;
22265
+ }
22266
+ return value;
22267
+ }
22268
+ function sha256(text) {
22269
+ return createHash2("sha256").update(text).digest("hex");
22270
+ }
22271
+ var evidenceCounter = 0;
22272
+ function nextEvidenceId() {
22273
+ evidenceCounter += 1;
22274
+ return `EV-${String(evidenceCounter).padStart(6, "0")}`;
22275
+ }
22276
+ function makeEvidence(input) {
22277
+ const artifacts = (input.artifacts ?? []).map((a) => {
22278
+ const content = redactSecrets(a.content);
22279
+ return { name: a.name, kind: a.kind, content, sha256: sha256(content) };
22280
+ });
22281
+ return {
22282
+ evidence_id: nextEvidenceId(),
22283
+ type: input.type,
22284
+ description: redactSecrets(input.description),
22285
+ source: input.source ? redactObject(input.source) : undefined,
22286
+ sink: input.sink ? redactObject(input.sink) : undefined,
22287
+ artifacts,
22288
+ recorded: new Date().toISOString()
22289
+ };
22290
+ }
22291
+
22292
+ // src/finding.ts
22293
+ var LIFECYCLE_TRANSITIONS = {
22294
+ detected: ["triaged", "rejected"],
22295
+ triaged: ["hypothesis", "rejected", "out_of_scope"],
22296
+ hypothesis: ["validating", "false_positive", "rejected", "blocked"],
22297
+ validating: ["confirmed", "likely", "unconfirmed", "false_positive", "blocked", "out_of_scope"],
22298
+ confirmed: [],
22299
+ false_positive: [],
22300
+ rejected: [],
22301
+ blocked: [],
22302
+ out_of_scope: []
22303
+ };
22304
+ function canTransition(from, to) {
22305
+ const allowed = LIFECYCLE_TRANSITIONS[from] ?? [];
22306
+ return allowed.includes(to);
22307
+ }
22308
+ var DEFAULT_WEIGHTS = {
22309
+ static_analysis: 0.2,
22310
+ data_flow: 0.25,
22311
+ reachability: 0.15,
22312
+ preconditions: 0.1,
22313
+ runtime_validation: 0.2,
22314
+ negative_control: 0.1
22315
+ };
22316
+ var confidenceWeights = { ...DEFAULT_WEIGHTS };
22317
+ function computeConfidence(factors) {
22318
+ let score = 0;
22319
+ for (const [key, weight] of Object.entries(confidenceWeights)) {
22320
+ if (factors[key] === true)
22321
+ score += weight;
22322
+ }
22323
+ return Math.round(score * 100) / 100;
22324
+ }
22325
+ function confidenceLevel(score) {
22326
+ if (score >= 0.9)
22327
+ return "confirmed";
22328
+ if (score >= 0.7)
22329
+ return "high_confidence";
22330
+ if (score >= 0.5)
22331
+ return "likely";
22332
+ if (score >= 0.3)
22333
+ return "suspected";
22334
+ return "informational";
22335
+ }
22336
+ var findingCounter = 0;
22337
+ function nextFindingId() {
22338
+ findingCounter += 1;
22339
+ return `BS-${new Date().getUTCFullYear()}-${String(findingCounter).padStart(6, "0")}`;
22340
+ }
22341
+ function makeFinding(input) {
22342
+ const now = new Date().toISOString();
22343
+ const status = input.status ?? "detected";
22344
+ const factors = confidenceFactorsFor(input);
22345
+ const confidence = computeConfidence(factors);
22346
+ return {
22347
+ id: nextFindingId(),
22348
+ status,
22349
+ title: input.title,
22350
+ target: input.target,
22351
+ classification: {
22352
+ severity: input.severity,
22353
+ cwe: input.cwe,
22354
+ cwe_name: input.cweName,
22355
+ cwe_confidence: input.cweConfidence
22356
+ },
22357
+ confidence,
22358
+ confidence_level: confidenceLevel(confidence),
22359
+ source: input.source,
22360
+ flow: input.flow ?? [],
22361
+ sink: input.sink,
22362
+ validation: { performed: false },
22363
+ evidence: [],
22364
+ chain: { id: input.chainId ?? null, name: input.chainName },
22365
+ impact: {},
22366
+ remediation: {},
22367
+ timestamps: { created: now, updated: now }
22368
+ };
22369
+ }
22370
+ function confidenceFactorsFor(_input) {
22371
+ return {
22372
+ static_analysis: true,
22373
+ data_flow: false,
22374
+ reachability: false,
22375
+ preconditions: false,
22376
+ runtime_validation: false,
22377
+ negative_control: false
22378
+ };
22379
+ }
22380
+
22228
22381
  // src/orchestrator.ts
22229
22382
  var _chainsCache = null;
22230
22383
  function loadChains() {
@@ -22393,15 +22546,41 @@ function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000,
22393
22546
  }
22394
22547
  report.tiers = { ...report.tiers, eagle_eye: { traced_chains: eagle } };
22395
22548
  const rank = { critical: 0, high: 1, medium: 2, low: 3 };
22396
- const findings = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
22549
+ const matchedSorted = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
22550
+ const findings = matchedSorted.map((f) => {
22551
+ const chain = chainById.get(f.chain_id);
22552
+ const severity = f.severity ?? "medium";
22553
+ const finding = makeFinding({
22554
+ title: f.name,
22555
+ target: { type: "source", path: target },
22556
+ severity,
22557
+ source: { type: "static_sink_signal", name: f.chain_id },
22558
+ sink: { type: f.chain_id, symbol: f.name },
22559
+ chainId: f.chain_id,
22560
+ chainName: f.name,
22561
+ status: "hypothesis"
22562
+ });
22563
+ const ev = makeEvidence({
22564
+ type: "sink_location",
22565
+ description: `Static sink signal matched escalation chain '${f.chain_id}' (${severity}). Hypothesis only — not yet validated.`,
22566
+ sink: { chain_id: f.chain_id, name: f.name, severity },
22567
+ artifacts: [{ name: "chain_match", kind: "json", content: JSON.stringify({ chain_id: f.chain_id, severity, steps: chain?.steps.length ?? 0 }) }]
22568
+ });
22569
+ finding.evidence.push(ev);
22570
+ return finding;
22571
+ });
22397
22572
  report.findings = findings.map((f) => ({
22398
- chain_id: f.chain_id,
22399
- name: f.name,
22400
- severity: f.severity,
22401
- status: "HYPOTHESIS"
22573
+ id: f.id,
22574
+ status: f.status,
22575
+ title: f.title,
22576
+ severity: f.classification.severity,
22577
+ confidence: f.confidence,
22578
+ confidence_level: f.confidence_level,
22579
+ chain_id: f.chain.id,
22580
+ evidence_count: f.evidence.length
22402
22581
  }));
22403
22582
  report.status = "COMPLETE";
22404
- report.note = "All findings are HYPOTHESES until verified live with strike_verify. " + "Apply each chain's negative_control before reporting.";
22583
+ report.note = "All findings are HYPOTHESES until verified live with strike_verify. " + "Apply each chain's negative_control before reporting. Confidence reflects static evidence only.";
22405
22584
  if (remember) {
22406
22585
  let captured = 0;
22407
22586
  for (const mc of blitz.matched_chains) {
@@ -23424,6 +23603,68 @@ function createServer() {
23424
23603
  }, async ({ topic, limit }) => {
23425
23604
  return { content: [{ type: "text", text: JSON.stringify(templateLookup(topic, limit ?? 10)) }] };
23426
23605
  });
23606
+ server.registerTool("finding_create", {
23607
+ title: "Create a canonical finding",
23608
+ description: "FINDINGS: create a canonical, evidence-first finding. Severity describes impact; confidence (deterministic) describes certainty. Default status=detected (hypothesis-pending).",
23609
+ inputSchema: {
23610
+ title: string2().describe("Finding title"),
23611
+ target_type: string2().optional().describe("Target type (web/api/source/mobile/network/other)"),
23612
+ host: string2().optional().describe("Target host"),
23613
+ endpoint: string2().optional().describe("Target endpoint/path"),
23614
+ severity: string2().describe("Severity: critical/high/medium/low/informational"),
23615
+ cwe: string2().optional().describe("CWE id (e.g. CWE-89)"),
23616
+ source_type: string2().describe("Source type (e.g. request_parameter, post_body, header, cookie, file)"),
23617
+ source_name: string2().describe("Source name"),
23618
+ sink_type: string2().describe("Sink type (e.g. sql_execution, command_execution, file_operations)"),
23619
+ sink_symbol: string2().optional().describe("Sink symbol (e.g. '->query(')"),
23620
+ chain_id: string2().optional().describe("Escalation chain id")
23621
+ }
23622
+ }, async ({ title, target_type, host, endpoint, severity, cwe, source_type, source_name, sink_type, sink_symbol, chain_id }) => {
23623
+ const f = makeFinding({
23624
+ title,
23625
+ target: { type: target_type ?? "web", host, endpoint },
23626
+ severity: severity ?? "medium",
23627
+ cwe,
23628
+ source: { type: source_type, name: source_name },
23629
+ sink: { type: sink_type, symbol: sink_symbol },
23630
+ chainId: chain_id ?? null,
23631
+ status: "detected"
23632
+ });
23633
+ return { content: [{ type: "text", text: JSON.stringify(f) }] };
23634
+ });
23635
+ server.registerTool("finding_transition", {
23636
+ title: "Advance a finding lifecycle",
23637
+ description: "FINDINGS: advance a finding through its strict lifecycle (detected->triaged->hypothesis->validating->confirmed). Rejects illegal transitions.",
23638
+ inputSchema: {
23639
+ status: string2().describe("Current status"),
23640
+ to: string2().describe("Target status (triaged/hypothesis/validating/confirmed/false_positive/rejected/blocked/out_of_scope)")
23641
+ }
23642
+ }, async ({ status, to }) => {
23643
+ const ok = canTransition(status, to);
23644
+ return { content: [{ type: "text", text: JSON.stringify({ from: status, to, legal: ok }) }] };
23645
+ });
23646
+ server.registerTool("confidence_score", {
23647
+ title: "Compute deterministic confidence",
23648
+ description: "FINDINGS: compute a deterministic weighted confidence score (static/data-flow/reachability/preconditions/validation/negative-control). Never an AI opinion.",
23649
+ inputSchema: {
23650
+ static_analysis: boolean2().optional(),
23651
+ data_flow: boolean2().optional(),
23652
+ reachability: boolean2().optional(),
23653
+ preconditions: boolean2().optional(),
23654
+ runtime_validation: boolean2().optional(),
23655
+ negative_control: boolean2().optional()
23656
+ }
23657
+ }, async (factors) => {
23658
+ const score = computeConfidence(factors);
23659
+ return { content: [{ type: "text", text: JSON.stringify({ score, level: confidenceLevel(score) }) }] };
23660
+ });
23661
+ server.registerTool("redact", {
23662
+ title: "Redact secrets from text",
23663
+ description: "EVIDENCE: redact passwords/API keys/tokens/cookies/private keys from text before persisting evidence or reporting.",
23664
+ inputSchema: { text: string2().describe("Text to redact") }
23665
+ }, async ({ text }) => {
23666
+ return { content: [{ type: "text", text: JSON.stringify({ redacted: redactSecrets(text) }) }] };
23667
+ });
23427
23668
  return server;
23428
23669
  }
23429
23670
  async function serve() {
@@ -23534,14 +23775,14 @@ function jsonMcpServersWrite(p) {
23534
23775
  };
23535
23776
  }
23536
23777
  function opencodeWrite(p) {
23537
- return (command, args) => {
23778
+ return (_command, _args) => {
23538
23779
  const dir = dirname(p);
23539
23780
  if (!existsSync6(dir))
23540
23781
  mkdirSync2(dir, { recursive: true });
23541
23782
  const existing = readJson(p) ?? {};
23542
23783
  existing.mcp = {
23543
23784
  ...existing.mcp ?? {},
23544
- blitzstrike: { type: "local", command: [command, ...args], enabled: true }
23785
+ blitzstrike: { type: "local", command: ["npx", "-y", "blitzstrike", "serve", "--mcp"], enabled: true }
23545
23786
  };
23546
23787
  writeFileSync(p, JSON.stringify(existing, null, 2));
23547
23788
  copyOpenCodeAgents();
@@ -0,0 +1,95 @@
1
+ # Findings & Evidence Engine
2
+
3
+ Blitz Strike follows one principle: **a scanner hit is a hypothesis; evidence is
4
+ the verdict.**
5
+
6
+ This document describes the canonical finding model, the strict lifecycle, and
7
+ the deterministic confidence engine. No other part of the codebase invents its
8
+ own finding shape — every producer funnels into these modules.
9
+
10
+ ## Finding schema
11
+
12
+ `src/finding.ts` defines the single `Finding` model:
13
+
14
+ | Field | Meaning |
15
+ | :--- | :--- |
16
+ | `id` | Canonical id, `BS-YYYY-NNNNNN` |
17
+ | `status` | Lifecycle state (see below) |
18
+ | `title` | Human-readable title |
19
+ | `target` | `type` (web/api/source/mobile/network/other) + host/endpoint/path |
20
+ | `classification` | severity, CWE id/name/confidence, CVSS (evidence-based) |
21
+ | `confidence` | 0.0–1.0 deterministic score (NOT an AI opinion) |
22
+ | `confidence_level` | informational / suspected / likely / high_confidence / confirmed |
23
+ | `source` | attacker-controlled source (type + name + location) |
24
+ | `flow` | data-flow path (source → transforms → sink) |
25
+ | `sink` | security-sensitive operation (type + symbol + location) |
26
+ | `validation` | performed + baseline + negative_control |
27
+ | `evidence` | array of integrity-tagged evidence records |
28
+ | `chain` | escalation chain id/name |
29
+ | `impact` / `remediation` | impact + fix |
30
+ | `timestamps` | created / updated |
31
+
32
+ ## Lifecycle
33
+
34
+ Strict, machine-readable state machine (`transition()` rejects illegal moves):
35
+
36
+ ```
37
+ detected → triaged → hypothesis → validating → confirmed
38
+ ↘ rejected ↘ false_positive
39
+ ↘ out_of_scope ↘ blocked
40
+ ↘ out_of_scope
41
+ ```
42
+
43
+ `confirmed`, `false_positive`, `rejected`, `blocked`, `out_of_scope` are
44
+ terminal.
45
+
46
+ ## Severity vs confidence
47
+
48
+ - **Severity** = impact (`critical` / `high` / `medium` / `low` / `informational`).
49
+ - **Confidence** = certainty (deterministic 0.0–1.0).
50
+
51
+ They are never combined. "Severity CRITICAL + confidence 0.41" = *potentially
52
+ critical, insufficient evidence*. "Severity MEDIUM + confidence 0.98" = *medium
53
+ impact, highly reliable*.
54
+
55
+ ## Confidence engine
56
+
57
+ `computeConfidence()` is deterministic and weighted (configurable via
58
+ `setConfidenceWeights()`):
59
+
60
+ | Factor | Default weight |
61
+ | :--- | :--- |
62
+ | static_analysis | 0.20 |
63
+ | data_flow | 0.25 |
64
+ | reachability | 0.15 |
65
+ | preconditions | 0.10 |
66
+ | runtime_validation | 0.20 |
67
+ | negative_control | 0.10 |
68
+
69
+ Levels: `0.00–0.29` informational · `0.30–0.49` suspected · `0.50–0.69` likely ·
70
+ `0.70–0.89` high_confidence · `0.90–1.00` confirmed.
71
+
72
+ A high confidence score is **never** proof by itself — confirmation requires
73
+ validation + evidence.
74
+
75
+ ## Evidence engine
76
+
77
+ `src/evidence.ts`:
78
+
79
+ - **Schema** — `evidence_id`, `type`, `description`, `source`, `sink`, `artifacts[]`, `recorded`.
80
+ - **Integrity** — every artifact carries a SHA-256 (`sha256()`), verified by `verifyEvidence()`.
81
+ - **Secret redaction** — `redactSecrets()` strips passwords, API keys, tokens,
82
+ cookies, `Authorization` headers, private keys before persistence/report/log/context.
83
+ - **Append-only** — confirmed evidence is never silently rewritten.
84
+
85
+ ## MCP tools
86
+
87
+ | Tool | Purpose |
88
+ | :--- | :--- |
89
+ | `finding_create` | create a canonical finding (default `detected`) |
90
+ | `finding_transition` | advance lifecycle (rejects illegal moves) |
91
+ | `confidence_score` | deterministic weighted confidence |
92
+ | `redact` | redact secrets from arbitrary text |
93
+
94
+ `run_engagement` now emits canonical findings (status `hypothesis`, carrying
95
+ static + sink evidence) instead of ad-hoc `{chain_id, name, severity}` tuples.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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": {