@blamejs/exceptd-skills 0.12.28 → 0.12.29

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/AGENTS.md CHANGED
@@ -139,7 +139,7 @@ Cross-cutting playbook `framework` is the natural correlation layer — many pla
139
139
 
140
140
  | Verb | What it does |
141
141
  |---|---|
142
- | `exceptd brief --all` | Grouped-by-scope summary of all 13 playbooks. `--scope <type>` filters. `--directives` expands directive IDs/titles per playbook. `--flat` for non-grouped. Legacy alias: `exceptd plan` (deprecated, scheduled for removal in v0.13). |
142
+ | `exceptd brief --all` | Grouped-by-scope summary of all 16 playbooks. `--scope <type>` filters. `--directives` expands directive IDs/titles per playbook. `--flat` for non-grouped. Legacy alias: `exceptd plan` (deprecated, scheduled for removal in v0.13). |
143
143
  | `exceptd brief <pb>` | Phase 2 threat-context briefing — threat context, RWEP thresholds, skill chain, token budget, jurisdiction obligations. |
144
144
  | `exceptd run <pb> --evidence <file>` | Phases 5-7 (analyze + validate + close) from agent evidence. Auto-detect cwd when no playbook positional. `--vex <file>` drops CycloneDX/OpenVEX `not_affected` CVEs. `--diff-from-latest` for drift mode. `--force-stale` overrides currency hard-block. |
145
145
  | `exceptd ai-run <pb>` | Streaming variant of `run` for AI agents; emits phase-by-phase NDJSON. |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.29 — 2026-05-15
4
+
5
+ Catalog hygiene + pipeline integrity pass. Closes Hard Rule #1, #6, #7, and #8 gaps that had accumulated across the 2025-2026 catalog growth; tightens the SBOM + OpenVEX + exit-code surfaces.
6
+
7
+ ### Features
8
+
9
+ **Compliance-theater test on every framework gap.** Every entry in the framework-control-gaps catalog (109 entries spanning NIST 800-53, ISO/IEC 27001/27017/27035/42001, SOC 2, UK CAF, AU ISM/Essential 8, EU DORA, EU NIS2, EU AI Act, HIPAA, PCI DSS, FedRAMP, CMMC, HITRUST, IEC 62443, OWASP, telecom standards, ransomware-class gaps, and OFAC sanctions screening) now carries a `theater_test` field with a falsifiable test that distinguishes paper compliance from actual security. Closes Hard Rule #6. Sample shape: `{claim, test, evidence_required[], verdict_when_failed: "compliance-theater"}`. The test must reference a concrete artifact (audit log, config dump, tabletop exercise stopwatch) whose result is binary.
10
+
11
+ **SBOM per-file SHA-256 + bundle digest.** `sbom.cdx.json` now includes `metadata.component.hashes[]` (bundle digest, SHA-256) and one `components[type=file]` entry per shipped file with its own SHA-256. Downstream supply-chain consumers can verify any individual file against the bundle. Excludes the regenerable `data/_indexes/` cache from per-file inventory (covered by the `Pre-computed indexes freshness` gate instead). Also corrects `metadata.tools` from the placeholder `name: "hand-written"` to the real generator script and bound package version.
12
+
13
+ **OpenVEX `author` threads operator attribution.** Previously hard-pinned to `"exceptd"`, which falsely attributed every disposition statement to the tooling vendor. Now mirrors the CSAF publisher.namespace fallback ladder: `runOpts.publisherNamespace` → `runOpts.operator` → `urn:exceptd:operator:unknown` with a `bundle_publisher_unclaimed` runtime warning. Operators running scans correctly own their dispositions.
14
+
15
+ **Exit code 10: UNKNOWN_COMMAND.** The dispatcher's unknown-command / missing-script / spawn-error paths previously exited 2, colliding with `EXIT_CODES.DETECTED_ESCALATE` semantics. Split into `EXIT_CODES.UNKNOWN_COMMAND = 10`. CI gates wiring `case 2)` for escalation triage no longer false-alarm on operator typos. Same regression class v0.12.24 closed for the SESSION_ID_COLLISION / RAN_NO_EVIDENCE code-3 collision.
16
+
17
+ **Reverse-reference auto-regeneration.** New `npm run refresh-reverse-refs` rebuilds the `skills_referencing` / `exceptd_skills` arrays on `data/atlas-ttps.json`, `data/cwe-catalog.json`, `data/d3fend-catalog.json`, and `data/rfc-references.json` from the manifest forward direction. Idempotent. A new `tests/reverse-ref-drift.test.js` blocks merges that leave the reverse direction out of sync with the manifest — eliminates the one-sided-reference drift class that audits have flagged repeatedly.
18
+
19
+ ### Bugs
20
+
21
+ - `crypto-codebase` `feeds_into` condition used the unsupported `contains` operator; the chain to the `secrets` playbook never fired. Replaced with `analyze.classification == 'detected'`. Same class of bug v0.12.28 corrected on the IR-cluster playbooks.
22
+ - Manifest `atlas_version` / `attack_version` had drifted to v5.1.0 / v17 while the data catalogs already pinned v5.4.0 / v19.0. Manifest now matches the catalogs and AGENTS.md ground truth.
23
+ - 14 sites in `bin/exceptd.js` used bare numeric `process.exitCode = 1` / `finish(1)` / `finish(0)` instead of `EXIT_CODES.*` constants. All migrated to the constant.
24
+ - `cmdCi` per-id loop called `runner.loadPlaybook(id)` without first running `validateIdComponent('playbook')` — a defense-in-depth gap relative to `cmdRunMulti`. Now validates before load.
25
+
26
+ ### Internal
27
+
28
+ - AI-discovery rate on `data/cve-catalog.json` moves 10% → 20% with three new flag flips backed by citations: CVE-2026-43284 + CVE-2026-43500 (Dirty Frag pair, Hyunwoo Kim with AI-assisted methodology per Sysdig); CVE-2026-46300 (Fragnesia, William Bowling using Zellic.io's AI agentic auditor). All other CVEs gain a `discovery_attribution_note` field citing the human researcher or vendor team. New `_meta.ai_discovery_methodology` block documents the 20%/30%/40% advancement ladder against the AGENTS.md Hard Rule #7 target. Gap to 40% explicitly tracked.
29
+ - AGENTS.md Quick Skill Reference: playbook count "all 13 playbooks" → "all 16 playbooks".
30
+ - `package.json.description`: "38 skills" → "42 skills".
31
+ - 22 reverse-reference entries across 4 catalogs cleaned up by the new regen script (atlas: 30 entries changed, cwe: 46, d3fend: 28, rfc: 22).
32
+ - Test suite 1064 → 1082 (six new test files: framework-gaps-theater-test-coverage, cve-ai-discovery-attribution, sbom-per-file-hash, reverse-ref-drift, plus updates to bin-dispatcher, cli-exit-codes, lib-exit-codes, cve-additions-v0-12-21 for the new contract).
33
+
34
+
3
35
  ## 0.12.28 — 2026-05-15
4
36
 
5
37
  Incident-response cluster — three new playbooks and skills covering identity-provider tenant compromise, cloud-IAM account takeover, and ransomware response. The existing `incident-response-playbook` skill stays as the generic PICERL backbone; the new surface adds attack-class-specific depth for the three IR scenarios that dominate 2025-2026 breach reporting.
package/bin/exceptd.js CHANGED
@@ -516,10 +516,11 @@ function main() {
516
516
  // Emit a structured JSON error matching the seven-phase verbs so operators
517
517
  // piping through `jq` get one consistent shape across the CLI surface.
518
518
  // emitError() sets exitCode + returns rather than calling process.exit()
519
- // so the stderr JSON drains before teardown; promote the exit code to 2
520
- // afterwards (unknown-command remains a distinct exit class).
519
+ // so the stderr JSON drains before teardown; promote the exit code to
520
+ // UNKNOWN_COMMAND (10) afterwards. Cycle 9 split this away from
521
+ // DETECTED_ESCALATE (2) — the two semantics had collided since v0.12.24.
521
522
  emitError(`unknown command "${cmd}"`, { hint: "Run `exceptd help` for the list of verbs.", verb: cmd });
522
- process.exitCode = 2;
523
+ process.exitCode = EXIT_CODES.UNKNOWN_COMMAND;
523
524
  return;
524
525
  }
525
526
 
@@ -530,7 +531,7 @@ function main() {
530
531
  `command "${cmd}" not available — expected ${path.relative(PKG_ROOT, script)} in the installed package.`,
531
532
  { verb: cmd }
532
533
  );
533
- process.exitCode = 2;
534
+ process.exitCode = EXIT_CODES.UNKNOWN_COMMAND;
534
535
  return;
535
536
  }
536
537
 
@@ -541,7 +542,7 @@ function main() {
541
542
  if (res.error) {
542
543
  // emitError + exitCode rather than stderr + exit() so the JSON drains.
543
544
  emitError(`failed to run ${cmd}: ${res.error.message}`, { verb: cmd });
544
- process.exitCode = 2;
545
+ process.exitCode = EXIT_CODES.UNKNOWN_COMMAND;
545
546
  return;
546
547
  }
547
548
  // Propagate the child's exit status via exitCode so any buffered output
@@ -615,7 +616,7 @@ function emit(obj, pretty, humanRenderer) {
615
616
  // and CI gates. The previous fix was per-verb; this is a universal catch
616
617
  // so new verbs / new ok:false paths can't regress the contract.
617
618
  if (obj && obj.ok === false && !process.exitCode) {
618
- process.exitCode = 1;
619
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
619
620
  }
620
621
  const wantJson = !!global.__exceptdWantJson || !!process.env.EXCEPTD_RAW_JSON;
621
622
  if (humanRenderer && !wantJson && !pretty) {
@@ -638,7 +639,7 @@ function emitError(msg, extra, pretty) {
638
639
  const body = Object.assign({ ok: false, error: msg }, extra || {});
639
640
  const s = pretty ? JSON.stringify(body, null, 2) : JSON.stringify(body);
640
641
  process.stderr.write(s + "\n");
641
- process.exitCode = 1;
642
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
642
643
  }
643
644
 
644
645
  /**
@@ -2107,7 +2108,7 @@ function cmdLint(runner, args, runOpts, pretty) {
2107
2108
  }
2108
2109
  return lines.join("\n");
2109
2110
  });
2110
- if (!ok) process.exitCode = 1;
2111
+ if (!ok) process.exitCode = EXIT_CODES.GENERIC_FAILURE;
2111
2112
  }
2112
2113
 
2113
2114
  function cmdBrief(runner, args, runOpts, pretty) {
@@ -3398,7 +3399,7 @@ function cmdIngest(runner, args, runOpts, pretty) {
3398
3399
 
3399
3400
  if (result && result.ok === false) {
3400
3401
  process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
3401
- process.exitCode = 1;
3402
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
3402
3403
  return;
3403
3404
  }
3404
3405
  emit(result, pretty);
@@ -5276,7 +5277,7 @@ function cmdDoctor(runner, args, runOpts, pretty) {
5276
5277
 
5277
5278
  if (wantJson) {
5278
5279
  emit(out, indent);
5279
- if (!allGreen) process.exitCode = 1;
5280
+ if (!allGreen) process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5280
5281
  return;
5281
5282
  }
5282
5283
 
@@ -5367,10 +5368,10 @@ function cmdDoctor(runner, args, runOpts, pretty) {
5367
5368
  process.stdout.write(`\n[doctor --fix] ${out.summary.fix_applied} — re-run \`exceptd doctor\` to confirm.\n`);
5368
5369
  } else if (out.summary.fix_attempted) {
5369
5370
  process.stdout.write(`\n[doctor --fix] ${out.summary.fix_attempted} (exit=${out.summary.fix_exit_code}); run \`node lib/sign.js generate-keypair\` manually.\n`);
5370
- process.exitCode = 1;
5371
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5371
5372
  return;
5372
5373
  }
5373
- if (errorList.length > 0) process.exitCode = 1;
5374
+ if (errorList.length > 0) process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5374
5375
  // Warnings alone do NOT force exit 1 — CI gates use exit 0 to mean "ran
5375
5376
  // successfully" even with informational warnings. Operators reading the
5376
5377
  // visible "[!! warn]" line still see the issue.
@@ -5502,7 +5503,7 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5502
5503
  // the framed error event so the stdout-only JSONL contract holds — host
5503
5504
  // AIs reading this stream must see structured frames, never bare text.
5504
5505
  process.stdout.write(JSON.stringify({ event: "error", reason: e.message, phase: "info", playbook_id: playbookId, directive_id: directiveId }) + "\n");
5505
- process.exitCode = 1;
5506
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5506
5507
  return;
5507
5508
  }
5508
5509
 
@@ -5589,7 +5590,7 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5589
5590
  // v0.12.12: same exit-after-write anti-pattern as the pre-stream
5590
5591
  // load path. Use exitCode + return so stderr drains.
5591
5592
  process.stderr.write((pretty ? JSON.stringify(result || {}, null, 2) : JSON.stringify(result || {})) + "\n");
5592
- process.exitCode = 1;
5593
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5593
5594
  return;
5594
5595
  }
5595
5596
  // v0.12.14: ai-run --no-stream previously emitted a
@@ -5696,7 +5697,7 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5696
5697
  catch (e) {
5697
5698
  handled = true;
5698
5699
  writeLine({ event: "error", reason: `invalid JSON on stdin: ${e.message}`, line_preview: line.slice(0, 120) });
5699
- return finish(1);
5700
+ return finish(EXIT_CODES.GENERIC_FAILURE);
5700
5701
  }
5701
5702
  if (!parsed || parsed.event !== "evidence" || !parsed.payload) {
5702
5703
  // Ignore non-evidence chatter so the host AI can interleave its own
@@ -5710,11 +5711,11 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5710
5711
  result = runner.run(playbookId, directiveId, submission, runOpts);
5711
5712
  } catch (e) {
5712
5713
  writeLine({ event: "error", reason: `runner threw: ${e.message}` });
5713
- return finish(1);
5714
+ return finish(EXIT_CODES.GENERIC_FAILURE);
5714
5715
  }
5715
5716
  if (!result || result.ok === false) {
5716
5717
  writeLine({ event: "error", reason: result?.reason || "runner returned ok:false", result });
5717
- return finish(1);
5718
+ return finish(EXIT_CODES.GENERIC_FAILURE);
5718
5719
  }
5719
5720
  writeLine({ phase: "detect", ...result.phases?.detect });
5720
5721
  writeLine({ phase: "analyze", ...result.phases?.analyze });
@@ -5759,7 +5760,7 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5759
5760
  }
5760
5761
  }
5761
5762
  writeLine({ event: "done", ok: true, session_id: result.session_id, evidence_hash: result.evidence_hash });
5762
- return finish(0);
5763
+ return finish(EXIT_CODES.SUCCESS);
5763
5764
  };
5764
5765
 
5765
5766
  // Handle empty/closed stdin: emit a hint then exit cleanly so AI agents
@@ -5767,7 +5768,7 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5767
5768
  // a hung process.
5768
5769
  if (process.stdin.isTTY) {
5769
5770
  writeLine({ event: "error", reason: "ai-run streaming mode requires evidence on stdin; pipe {\"event\":\"evidence\",\"payload\":{...}} or use --no-stream." });
5770
- process.exitCode = 1;
5771
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5771
5772
  return;
5772
5773
  }
5773
5774
 
@@ -5803,7 +5804,7 @@ function cmdAiRun(runner, args, runOpts, pretty) {
5803
5804
  } catch { /* fall through to error */ }
5804
5805
  }
5805
5806
  writeLine({ event: "error", reason: "stdin closed without an evidence event. Pipe `{\"event\":\"evidence\",\"payload\":{...}}` for streaming mode, or pass --no-stream + --evidence <file> for single-shot." });
5806
- process.exitCode = 1;
5807
+ process.exitCode = EXIT_CODES.GENERIC_FAILURE;
5807
5808
  return;
5808
5809
  }
5809
5810
  });
@@ -6061,6 +6062,15 @@ function cmdCi(runner, args, runOpts, pretty) {
6061
6062
  let clockStartedReasons = [];
6062
6063
 
6063
6064
  for (const id of ids) {
6065
+ // Cycle 9 B4: defense-in-depth — validate id even though the catalog-iter
6066
+ // upstream is trusted. A corrupt catalog returning a malformed id would
6067
+ // otherwise reach loadPlaybook unchecked. Matches the cmdRunMulti pattern.
6068
+ const idCheck = validateIdComponent(id, "playbook");
6069
+ if (!idCheck.ok) {
6070
+ results.push({ playbook_id: id, ok: false, error: idCheck.reason });
6071
+ fail = true;
6072
+ continue;
6073
+ }
6064
6074
  let pb;
6065
6075
  try { pb = runner.loadPlaybook(id); }
6066
6076
  catch (e) { results.push({ playbook_id: id, ok: false, error: e.message }); fail = true; continue; }
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-15T23:31:25.676Z",
3
+ "generated_at": "2026-05-16T01:17:57.741Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "41d91731d616c0e2514783212f7eb32761298ab40cde66aa6076724aff729190",
8
- "data/atlas-ttps.json": "db52a797f6ba7c9a61fd7b1225ebbc268ddf21abe29a106c4246c2ed2e617b86",
7
+ "manifest.json": "5b9c21d9c2d885b439990b7b499429bf8a59e69cc737abfb386aa0255f2e8228",
8
+ "data/atlas-ttps.json": "259e76e4252c7a56c17bbe96982a5e37ac89131c2d37a547fe38d64dcacfd763",
9
9
  "data/attack-techniques.json": "51f60819aef36e960fd768e44dcc725e137781534fbbb028e5ef6baa21defa1d",
10
- "data/cve-catalog.json": "a2acad16f5e3856b07019fa00110e9dcb38ec5cc71b318d0e164bfcba7f4f644",
11
- "data/cwe-catalog.json": "19893d2a7139d86ff3fcf296b0e6cda10e357727a1d1ffb56af282104e99157a",
12
- "data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
10
+ "data/cve-catalog.json": "72164f10238b4ba26a6c2fcacd0dfa3e745cd83b7c7c525ca26d8069511a4f24",
11
+ "data/cwe-catalog.json": "b6d1a950e9dec8b313f65a546dcff724bf27d3717deca74decc04a6ba15d4538",
12
+ "data/d3fend-catalog.json": "ac7c1b0ba5cc84754264846b8173011ca4328773dd981c7b42599e112e54b3c4",
13
13
  "data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
14
14
  "data/exploit-availability.json": "a9eeda95d24b56c28a0d0178fc601b531653e2ba7dc857160b35ad23ad6c7471",
15
- "data/framework-control-gaps.json": "8d6cbf6c8fc38060c5cea9f300a61b4d0cbbda5e490983bd6780d0b0ae841e5a",
15
+ "data/framework-control-gaps.json": "0c4aa0d3c48da3b3a88d0b9faa078003af76a809b63d00f1da1c504738872a06",
16
16
  "data/global-frameworks.json": "0168825497e03f079274c9da2e5529310a2ba5bd7c7da7c93acd0b66ed845b8a",
17
- "data/rfc-references.json": "a11de1bcff62b8f5e0bb8ce47a9b3fa26cf733ba283a8f1c9c4185d74efaad3e",
17
+ "data/rfc-references.json": "e90ec6755f6a670fdb589bbdc61b3010c90531da46065ada377272f34d282fcb",
18
18
  "data/zeroday-lessons.json": "d960e5f8ca7a83c10194cd60207e13046a7eee1b8793e2f3de79475db283f800",
19
19
  "skills/kernel-lpe-triage/skill.md": "8e94bfd38d6db47342fbbe95a0c8df8f7c38743982c13e9de6a1c59cd3783d33",
20
20
  "skills/ai-attack-surface/skill.md": "13e543fc92b9b27cdb647dce96a9eeb44919e0fa92ec41e8265a9981a23e7b79",
@@ -89,6 +89,13 @@
89
89
  "schema_version": "1.1.0",
90
90
  "entry_count": 15
91
91
  },
92
+ {
93
+ "date": "2026-05-15",
94
+ "type": "manifest_review",
95
+ "artifact": "manifest.json",
96
+ "path": "manifest.json",
97
+ "note": "manifest threat_review_date — 42 skills, 11 catalogs"
98
+ },
92
99
  {
93
100
  "date": "2026-05-13",
94
101
  "type": "catalog_update",
@@ -386,13 +393,6 @@
386
393
  "artifact": "security-maturity-tiers",
387
394
  "path": "skills/security-maturity-tiers/skill.md",
388
395
  "note": "Three-tier implementation roadmap — MVP (ship this week), Practical (scalable today), Overkill (defense-in-depth)"
389
- },
390
- {
391
- "date": "2026-05-01",
392
- "type": "manifest_review",
393
- "artifact": "manifest.json",
394
- "path": "manifest.json",
395
- "note": "manifest threat_review_date — 42 skills, 11 catalogs"
396
396
  }
397
397
  ]
398
398
  }
@@ -1367,7 +1367,7 @@
1367
1367
  },
1368
1368
  "CVE-2026-43284": {
1369
1369
  "name": "Dirty Frag (ESP/IPsec component)",
1370
- "rwep": 38,
1370
+ "rwep": 53,
1371
1371
  "cvss": 8.8,
1372
1372
  "cisa_kev": false,
1373
1373
  "epss_score": 0.00007,
@@ -1577,7 +1577,7 @@
1577
1577
  },
1578
1578
  "CVE-2026-43500": {
1579
1579
  "name": "Dirty Frag (RxRPC component)",
1580
- "rwep": 32,
1580
+ "rwep": 47,
1581
1581
  "cvss": 7.6,
1582
1582
  "cisa_kev": false,
1583
1583
  "epss_score": 0.0001,
@@ -1821,7 +1821,7 @@
1821
1821
  },
1822
1822
  "CVE-2026-46300": {
1823
1823
  "name": "Fragnesia",
1824
- "rwep": 20,
1824
+ "rwep": 35,
1825
1825
  "cvss": 7.8,
1826
1826
  "cisa_kev": false,
1827
1827
  "referencing_skills": [],