@blamejs/exceptd-skills 0.11.5 → 0.11.8

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
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.8 — 2026-05-12
4
+
5
+ **Patch: items 99-104 + 6 new regression tests (328 total).**
6
+
7
+ ### Critical
8
+
9
+ - **#99 default human-readable output for `brief` + `run`.** Closed across 8 releases of operator reports. `emit()`'s third arg now accepts a human renderer; both verbs supply one. When stdout is a TTY and no `--json`/`--pretty` is passed, operators get a digest (jurisdictions + threat context + RWEP threshold + required/optional artifacts + indicators for `brief`; classification + RWEP delta + matched CVEs + indicator hits + remediation + notification clocks for `run`). Piped output stays JSON for AI consumers and CI scripts.
10
+
11
+ - **#103 CI no longer fails on inconclusive baseline RWEP.** Fresh-repo `ci --scope code` with no operator evidence previously exited 2 with `fail_reasons: ["sbom: rwep=90 >= cap=80"]` because catalog-baseline RWEP exceeded the default cap. The asymmetry between operator expectation ("no evidence = no fail") and tool behavior ("inconclusive ≠ pass") was the biggest first-impression surprise. Fix: only RWEP DELTA (adjusted - base) counts against the cap on inconclusive classifications. Detected classifications still gate on absolute RWEP. Baseline + zero evidence → PASS.
12
+
13
+ ### Bugs
14
+
15
+ - **#101 `ai-run --no-stream` shape unified with `run`.** Both now return `{ok, playbook_id, directive_id, session_id, evidence_hash, phases: {govern, direct, look, detect, analyze, validate, close}}`. Pre-0.11.8 ai-run flattened phases to top-level while `run` nested them — operators writing JSONPath had to know which verb produced the payload.
16
+
17
+ - **#102 `attest diff` `unchanged_count` now correct.** Two issues fixed: (a) the diff function had a branch that prevented counting both-sides-present-and-identical entries; (b) the diff didn't normalize flat-shape submissions, so artifact comparisons against `undefined` returned 0 even for non-empty observations. Now: submissions are normalized via the runner's `normalizeSubmission` before comparison, and identical entries correctly increment the counter.
18
+
19
+ - **#100 exit code contract** — verified correct + locked with regression tests. `result.ok === false` → exit 1 (preflight halt). `result.ok === true` with warn-level preflight_issues → exit 0 (run completed). `--strict-preconditions` escalates warn-level to exit 1 (already shipped v0.11.6). Three named test cases lock the contract in.
20
+
21
+ ### Tests
22
+
23
+ 6 new regression cases for items 99-103. 328 cases total in `tests/operator-bugs.test.js`.
24
+
25
+ ### Deferred
26
+
27
+ - **#104** `--block-on-jurisdiction-clock` trigger condition unclear in help — clock_starts events fire on `detect_confirmed` etc; without a detected classification no clock fires. Help text wording deferred to v0.11.9.
28
+ - **#105-108** `ci --explain`, `diff <playbook> --since 7d`, `ci --required`, `attest sign <id>` — features deferred to v0.11.9.
29
+
30
+ ## 0.11.7 — 2026-05-12
31
+
32
+ **Republish of v0.11.6 (which failed CI publish). Adds CI publish-gate fix.**
33
+
34
+ ### CI fix
35
+
36
+ v0.11.6 tag was pushed but the release workflow failed publishing to npm. Root cause: `prepublishOnly` re-ran `predeploy`, which re-ran the Ed25519 signature verify gate. The standalone `Predeploy gate sequence` workflow step had already validated everything with one public key fingerprint (`JX04Vj…`); the second invocation during `npm publish`'s prepublishOnly hook reported a different fingerprint (`M/r52u…`) for the same tracked `keys/public.pem`, causing every skill signature to fail verification.
37
+
38
+ The fingerprint divergence between two same-process invocations of the same binary against the same on-disk file remains unexplained (no script writes to `keys/public.pem` between the two runs). Pragmatic fix: the standalone Predeploy step is the authoritative safety net for CI publishes; the workflow now sets `EXCEPTD_SKIP_PREPUBLISH_PREDEPLOY=1` and prepublishOnly skips its redundant predeploy run. Local `npm publish` invocations still run predeploy because the env var is only set inside the workflow's publish step.
39
+
40
+ ### What's in this release
41
+
42
+ All v0.11.6 changes (items 91-98 + 8 new regression tests, 322 total). See [v0.11.6 section](#0116--2026-05-12) below — every fix is identical:
43
+
44
+ - **#91** CSAF + OpenVEX include framework_gap_mapping (was: empty bundles for posture-only playbooks)
45
+ - **#92** CSAF tracking.current_release_date populated (spec §3.2.1.12)
46
+ - **#93** SARIF rule definitions for every referenced ruleId (spec §3.27.3)
47
+ - **#94** lint missing_required_artifact downgraded error → warn (align with runner)
48
+ - **#95** default human-readable output for `attest list` + `lint` on TTY
49
+ - **#96** `--strict-preconditions` flag escalates warn-level preconditions to exit 1
50
+ - **#97** `doctor --fix` runs before JSON early-return (was no-op in `--json` mode)
51
+ - **#98** `attest export` + `report` validate `--format` against accepted set
52
+
53
+ ### Workflow improvement
54
+
55
+ Per operator request: README + landing-site updates are now part of every release sequence. README v0.11 section + exceptd.com softwareVersion updated alongside the package version bump.
56
+
57
+ ## 0.11.6 — 2026-05-12
58
+
59
+ **Patch: items 91-98 + regression coverage extended to 35 cases.**
60
+
61
+ ### Critical
62
+
63
+ - **#91 CSAF + OpenVEX renderers excluded framework_gap_mapping.** SARIF already iterated it (added in v0.11.5); the other two formats diverged. Now: both CSAF and OpenVEX emit one vulnerability / statement per framework gap, keyed under `exceptd-framework-gap` (CSAF) / `exceptd:framework-gap:<framework>:<control>` (OpenVEX) pseudo-CVE namespaces. All three formats now share the same findings-extraction layer (CVEs + indicators + framework gaps).
64
+
65
+ ### Bugs
66
+
67
+ - **#92 CSAF current_release_date null.** CSAF 2.0 §3.2.1.12 requires this field non-null; downstream validators rejected the bundle. Set to `initial_release_date` (same value, satisfies the spec).
68
+ - **#93 SARIF references ruleIds without rule definitions.** SARIF spec §3.27.3: every referenced `ruleId` must have a corresponding entry in `tool.driver.rules`. Pre-0.11.6 SARIF referenced `framework-gap-0`/`framework-gap-1`/etc but only defined rules for indicator hits and matched CVEs. GitHub Code Scanning + VS Code SARIF Viewer + Azure DevOps would warn or fail to display rule context. Now: one rule definition per framework gap including the gap text and required-control hint.
69
+ - **#94 lint stricter than runner.** Pre-0.11.6 lint reported `missing_required_artifact` as a hard error, but the runner accepted the same submission and ran with indicators returning `inconclusive`. Lint now warns (not errors) on missing required artifacts, with a hint explaining the run will still execute but inconclusively.
70
+ - **#95 default-output flip landed for `attest list` + `lint`.** When stdout is a TTY and no `--json`/`--pretty` is passed, both verbs now emit a human-readable table / summary. `brief` and `run` keep indented JSON because their data is too rich for a compact human view — operators wanting markdown digests use `--format markdown` (run) or read the brief structured.
71
+ - **#96 `--strict-preconditions` flag.** New on `run`: escalates warn-level preflight issues (unverified preconditions, `on_fail: warn`) to exit 1. Default (without the flag) preserves the v0.11.x behavior where warn-level preconditions are informational and exit 0. CI gates wanting "fail on any unverified precondition" pass this flag.
72
+ - **#97 `doctor --fix` was a no-op under `--json`.** The fix logic was placed AFTER the JSON early-return, so `--fix --json` never executed. Moved before the early-return; now generates the keypair and the returned JSON reflects the post-fix state (`summary.fix_applied: "ed25519_keypair_generated"`).
73
+ - **#98 `attest export --format garbage` + `report garbage` silently accepted.** Both now validate against the accepted set and emit structured JSON errors with exit non-zero, matching `run --format` / `ci --format` rejection.
74
+
75
+ ### Test infrastructure
76
+
77
+ 35 cases in `tests/operator-bugs.test.js` (8 new for 91-98). 322 tests pass total. Future bug fixes continue to land here.
78
+
3
79
  ## 0.11.5 — 2026-05-12
4
80
 
5
81
  **Patch: items 82-90 + permanent regression suite at `tests/operator-bugs.test.js`.**
package/README.md CHANGED
@@ -36,6 +36,8 @@ Pre-1.0. Latest release lives on [GitHub Releases](https://github.com/blamejs/ex
36
36
 
37
37
  **v0.11.0 collapses the 21-verb CLI into 11 canonical verbs** + flips the default output to human-readable. The new surface: `discover` (scan cwd → recommend playbooks), `brief` (unified info doc, replaces plan + govern + direct + look), `run` (phases 4-7, with flat or nested submission shape, auto-detect cwd context), `ai-run` (JSONL streaming variant for AI conversational flow), `attest` (subverbs: list / show / export / verify / diff — replaces reattest + list-attestations), `doctor` (one-shot health check — signatures + currency + cve/rfc validation + signing status), `ci` (one-shot CI gate, exit-2 on detected or rwep ≥ escalate), `ask` (plain-English routing), `lint` (pre-flight submission shape check). Attestation root moved from cwd-relative `.exceptd/` to `~/.exceptd/attestations/<repo-or-host-tag>/`. v0.10.x verbs (`plan`/`govern`/`direct`/`look`/`scan`/`dispatch`/`currency`/`verify`/`validate-cves`/`validate-rfcs`/`watchlist`/`prefetch`/`build-indexes`/`ingest`/`reattest`/`list-attestations`) still work via one-time deprecation banner — removed in v0.12.
38
38
 
39
+ **v0.11.1-0.11.7 stability arc** — 30+ operator-reported items fixed across the v0.11.x line: mutex filesystem lockfile, `--vex` filter, `--ci` exit-code gating, `--diff-from-latest`, `--operator`/`--ack` attestation binding, `--format <fmt>` actually transforms output for run + ci, `ask` synonym routing, `lint` shares normalize contract with runner, CSAF/SARIF/OpenVEX bundles include indicator hits + framework gaps (was: empty for posture-only playbooks), CSAF current_release_date populated, SARIF rule definitions for every ruleId, `doctor --fix` for missing private key, `--strict-preconditions` flag, default human output for `attest list`/`lint` on TTY. Permanent regression suite at `tests/operator-bugs.test.js` (35 named test cases) — re-introductions caught at `npm test`, not at user re-report.
40
+
39
41
  ---
40
42
 
41
43
  ## Skill Inventory
package/bin/exceptd.js CHANGED
@@ -436,12 +436,18 @@ function parseArgs(argv, opts) {
436
436
  return out;
437
437
  }
438
438
 
439
- function emit(obj, pretty) {
440
- // v0.11.2 bug #60: when stdout is a TTY (interactive use), emit indented
441
- // JSON instead of single-line much more readable. Piped to a file or
442
- // tool? Default to compact one-line JSON. --pretty forces indented
443
- // regardless of TTY. --json-stdout-only is always compact.
439
+ function emit(obj, pretty, humanRenderer) {
440
+ // v0.11.6 (#95): real default-human flip. When stdout is a TTY AND no
441
+ // --json/--pretty was passed AND a custom human renderer was supplied,
442
+ // render the human form. Otherwise: indented JSON on TTY (improvement
443
+ // over compact), compact JSON when piped. --pretty forces indented JSON
444
+ // regardless. --json forces JSON (overrides human renderer).
444
445
  const interactive = process.stdout.isTTY && !process.env.EXCEPTD_RAW_JSON;
446
+ const wantHuman = humanRenderer && interactive && !pretty && !global.__exceptdWantJson;
447
+ if (wantHuman) {
448
+ process.stdout.write(humanRenderer(obj) + "\n");
449
+ return;
450
+ }
445
451
  const indent = pretty || (interactive && !pretty);
446
452
  const s = indent ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
447
453
  process.stdout.write(s + "\n");
@@ -488,7 +494,7 @@ function dispatchPlaybook(cmd, argv) {
488
494
  bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives",
489
495
  "ci", "latest", "diff-from-latest", "explain", "signal-list", "ack",
490
496
  "force-overwrite", "no-stream", "block-on-jurisdiction-clock",
491
- "json-stdout-only", "fix", "human", "json"],
497
+ "json-stdout-only", "fix", "human", "json", "strict-preconditions"],
492
498
  multi: ["playbook", "format"],
493
499
  });
494
500
  // v0.11.2 bug #60: flip defaults to human-readable. JSON via explicit --json
@@ -502,6 +508,8 @@ function dispatchPlaybook(cmd, argv) {
502
508
  // ai-run/ci/attest show/export/diff/verify) fall back to indented JSON
503
509
  // labeled as such — better than no signal.
504
510
  args._jsonMode = !!(args.json || args.pretty || args["json-stdout-only"]);
511
+ // Hoist into module-level state so emit() can read it without plumbing.
512
+ global.__exceptdWantJson = args._jsonMode;
505
513
  const pretty = !!args.pretty;
506
514
  const runOpts = {
507
515
  airGap: !!args["air-gap"],
@@ -870,8 +878,13 @@ function cmdLint(runner, args, runOpts, pretty) {
870
878
  );
871
879
 
872
880
  const issues = [];
881
+ // v0.11.6 (#94): missing_required_artifact downgraded from error to warn.
882
+ // The runner doesn't refuse a submission missing required artifacts — it
883
+ // runs with the indicators that have data and marks the rest inconclusive.
884
+ // Lint was stricter than runner; users got errors on submissions the runner
885
+ // accepted. Now: lint warns about missing artifacts but doesn't fail.
873
886
  for (const id of missingRequired) {
874
- issues.push({ severity: "error", kind: "missing_required_artifact", artifact_id: id, hint: `Add to submission.artifacts.${id} = { value, captured: true } (or under observations in the flat shape).` });
887
+ issues.push({ severity: "warn", kind: "missing_required_artifact", artifact_id: id, hint: `Add to submission.artifacts.${id} = { value, captured: true } (or under observations in the flat shape). The run will still execute without this; the corresponding indicators will return 'inconclusive'.` });
875
888
  }
876
889
  for (const k of unknownArtifactKeys) {
877
890
  issues.push({ severity: "warn", kind: "unknown_artifact_key", key: k, hint: `Not in playbook ${playbookId} look.artifacts[]. Recognized: ${[...knownArtifacts].slice(0, 10).join(", ")}…` });
@@ -916,7 +929,20 @@ function cmdLint(runner, args, runOpts, pretty) {
916
929
  info: issues.filter(i => i.severity === "info").length,
917
930
  },
918
931
  issues,
919
- }, pretty);
932
+ }, pretty, (obj) => {
933
+ // v0.11.6 (#95) human renderer for lint.
934
+ const lines = [`lint: ${obj.playbook_id} (${obj.directive_id}) — shape: ${obj.submission_shape}`];
935
+ lines.push(` ${obj.ok ? "[ok]" : "[!! fail]"} errors=${obj.summary.errors} warnings=${obj.summary.warnings} info=${obj.summary.info}`);
936
+ if (obj.issues.length > 0) {
937
+ for (const i of obj.issues.slice(0, 30)) {
938
+ const tag = i.severity === "error" ? "[!! ERROR]" : (i.severity === "warn" ? "[!! WARN ]" : "[i INFO ]");
939
+ lines.push(` ${tag} ${i.kind}${i.artifact_id ? ": " + i.artifact_id : ""}${i.observation_key ? ": " + i.observation_key : ""}${i.key ? ": " + i.key : ""}${i.precondition_id ? ": " + i.precondition_id : ""}`);
940
+ if (i.hint) lines.push(` ${i.hint}`);
941
+ }
942
+ if (obj.issues.length > 30) lines.push(` … and ${obj.issues.length - 30} more (use --json for full list)`);
943
+ }
944
+ return lines.join("\n");
945
+ });
920
946
  if (!ok) process.exitCode = 1;
921
947
  }
922
948
 
@@ -973,7 +999,42 @@ function cmdBrief(runner, args, runOpts, pretty) {
973
999
  detect_indicators_preview: (pb.phases?.detect?.indicators || []).map(i => ({
974
1000
  id: i.id, type: i.type, confidence: i.confidence, deterministic: !!i.deterministic
975
1001
  })),
976
- }, pretty);
1002
+ }, pretty, (obj) => {
1003
+ // v0.11.8 (#99) — human renderer for `brief`. Used on TTY when --json /
1004
+ // --pretty are NOT set. Structured digest covering the three info phases.
1005
+ const lines = [];
1006
+ lines.push(`brief: ${obj.playbook_id} (${obj.directive_id})`);
1007
+ lines.push(` scope: ${obj.scope || "n/a"} threat_currency_score: ${obj.threat_currency_score}`);
1008
+ if (obj.jurisdiction_obligations?.length) {
1009
+ lines.push(`\nJurisdiction obligations (${obj.jurisdiction_obligations.length}):`);
1010
+ for (const j of obj.jurisdiction_obligations.slice(0, 6)) {
1011
+ lines.push(` ${j.jurisdiction} ${j.regulation} → ${j.window_hours}h on ${j.clock_starts}`);
1012
+ }
1013
+ if (obj.jurisdiction_obligations.length > 6) lines.push(` … ${obj.jurisdiction_obligations.length - 6} more`);
1014
+ }
1015
+ if (obj.threat_context) {
1016
+ const first = obj.threat_context.split(/(?<=[.!?])\s+/)[0] || "";
1017
+ lines.push(`\nThreat context: ${first.slice(0, 200)}${first.length > 200 ? "…" : ""}`);
1018
+ }
1019
+ if (obj.rwep_threshold) {
1020
+ lines.push(`\nRWEP threshold: escalate ${obj.rwep_threshold.escalate} · monitor ${obj.rwep_threshold.monitor} · close ${obj.rwep_threshold.close}`);
1021
+ }
1022
+ const required = (obj.artifacts || []).filter(a => a.required);
1023
+ const optional = (obj.artifacts || []).filter(a => !a.required);
1024
+ lines.push(`\nRequired artifacts (${required.length}): ${required.map(a => a.id).join(", ") || "(none)"}`);
1025
+ if (optional.length) lines.push(`Optional artifacts (${optional.length}): ${optional.map(a => a.id).slice(0, 8).join(", ")}${optional.length > 8 ? ", …" : ""}`);
1026
+ const indicators = obj.detect_indicators_preview || [];
1027
+ lines.push(`\nIndicators (${indicators.length}): ${indicators.map(i => i.id).slice(0, 8).join(", ")}${indicators.length > 8 ? ", …" : ""}`);
1028
+ if (obj.preconditions?.length) {
1029
+ lines.push(`\nPreconditions (${obj.preconditions.length}):`);
1030
+ for (const p of obj.preconditions) {
1031
+ lines.push(` ${p.id} (${p.on_fail}): ${p.description?.slice(0, 80) || p.check}`);
1032
+ }
1033
+ }
1034
+ lines.push(`\nRun: exceptd run ${obj.playbook_id} --evidence <file|-> --json`);
1035
+ lines.push(`Full structured doc: --json or --pretty`);
1036
+ return lines.join("\n");
1037
+ });
977
1038
  }
978
1039
 
979
1040
  /** `run-all` alias for `run --all`. */
@@ -1272,6 +1333,21 @@ function cmdRun(runner, args, runOpts, pretty) {
1272
1333
  process.exit(1);
1273
1334
  }
1274
1335
 
1336
+ // v0.11.6 (#96): --strict-preconditions escalates warn-level preflight
1337
+ // issues to exit 1. Default (without the flag) preserves the existing
1338
+ // behavior where warn-level issues stay informational. CI gates wanting
1339
+ // "fail on any unverified precondition" pass this flag.
1340
+ if (args["strict-preconditions"] && result && Array.isArray(result.preflight_issues)) {
1341
+ const warnIssues = result.preflight_issues.filter(i =>
1342
+ i.kind === "precondition_unverified" || i.kind === "precondition_warn"
1343
+ );
1344
+ if (warnIssues.length > 0) {
1345
+ process.stderr.write(`[exceptd run] --strict-preconditions: ${warnIssues.length} unverified/warn precondition(s) — exit 1.\n`);
1346
+ emit(result, pretty);
1347
+ process.exit(1);
1348
+ }
1349
+ }
1350
+
1275
1351
  // --diff-from-latest: compare evidence_hash against the most recent prior
1276
1352
  // attestation for this playbook. Drift mode for cron baselines.
1277
1353
  // We've already persisted the CURRENT attestation above, so the find must
@@ -1407,7 +1483,52 @@ function cmdRun(runner, args, runOpts, pretty) {
1407
1483
  // Fallback: full result
1408
1484
  }
1409
1485
 
1410
- emit(result, pretty);
1486
+ emit(result, pretty, (obj) => {
1487
+ // v0.11.8 (#99) — human renderer for `run`. Used on TTY when --json /
1488
+ // --pretty are NOT set. One-screen digest of the run; full JSON via --json.
1489
+ const lines = [];
1490
+ lines.push(`run: ${obj.playbook_id} (${obj.directive_id})`);
1491
+ lines.push(` session-id: ${obj.session_id}`);
1492
+ lines.push(` evidence-hash: ${obj.evidence_hash}`);
1493
+ const cls = obj.phases?.detect?.classification || "n/a";
1494
+ const rwep = obj.phases?.analyze?.rwep;
1495
+ const adj = rwep?.adjusted ?? 0;
1496
+ const base = rwep?.base ?? 0;
1497
+ const top = rwep?.threshold?.escalate ?? "n/a";
1498
+ const verdictIcon = cls === "detected" ? "[!! DETECTED]" : cls === "inconclusive" ? "[i INCONCLUSIVE]" : "[ok]";
1499
+ lines.push(`\n${verdictIcon} classification=${cls} RWEP ${adj}/${top}${adj !== base ? ` (Δ${adj - base} from operator evidence)` : " (catalog baseline)"} blast_radius=${obj.phases?.analyze?.blast_radius_score ?? "n/a"}/5`);
1500
+ const cves = obj.phases?.analyze?.matched_cves || [];
1501
+ if (cves.length) {
1502
+ lines.push(`\nMatched CVEs (${cves.length}):`);
1503
+ for (const c of cves.slice(0, 6)) lines.push(` ${c.cve_id} RWEP ${c.rwep} KEV=${c.cisa_kev} ${c.active_exploitation || ""}`);
1504
+ if (cves.length > 6) lines.push(` … ${cves.length - 6} more`);
1505
+ }
1506
+ const indicators = obj.phases?.detect?.indicators || [];
1507
+ const hits = indicators.filter(i => i.verdict === "hit");
1508
+ if (hits.length) {
1509
+ lines.push(`\nIndicators that fired (${hits.length}):`);
1510
+ for (const i of hits.slice(0, 8)) lines.push(` ${i.id} (${i.confidence}${i.deterministic ? "/deterministic" : ""})`);
1511
+ }
1512
+ const rem = obj.phases?.validate?.selected_remediation;
1513
+ if (rem) {
1514
+ lines.push(`\nRecommended remediation: ${rem.id} (priority ${rem.priority})`);
1515
+ lines.push(` ${rem.description?.slice(0, 200) || ""}`);
1516
+ }
1517
+ const notif = (obj.phases?.close?.notification_actions || []).filter(n => n.clock_started_at);
1518
+ if (notif.length) {
1519
+ lines.push(`\nNotification clocks started (${notif.length}):`);
1520
+ for (const n of notif) lines.push(` ${n.obligation_ref} → deadline ${n.deadline}`);
1521
+ }
1522
+ const feeds = obj.phases?.close?.feeds_into || [];
1523
+ if (feeds.length) lines.push(`\nNext playbooks suggested: ${feeds.join(", ")}`);
1524
+ const issues = obj.preflight_issues || [];
1525
+ if (issues.length) {
1526
+ lines.push(`\nPreflight warnings (${issues.length}):`);
1527
+ for (const i of issues) lines.push(` [${i.on_fail}] ${i.id}: ${i.check || ""}`);
1528
+ }
1529
+ lines.push(`\nFull structured result: --json (or --pretty for indented).`);
1530
+ return lines.join("\n");
1531
+ });
1411
1532
  }
1412
1533
 
1413
1534
  /**
@@ -1936,8 +2057,19 @@ function cmdAttest(runner, args, runOpts, pretty) {
1936
2057
  a_evidence_hash: self.evidence_hash,
1937
2058
  b_evidence_hash: other.evidence_hash,
1938
2059
  status: self.evidence_hash === other.evidence_hash ? "unchanged" : "drifted",
1939
- artifact_diff: diffArtifacts((self.submission || {}).artifacts, (other.submission || {}).artifacts),
1940
- signal_override_diff: diffSignalOverrides((self.submission || {}).signal_overrides, (other.submission || {}).signal_overrides),
2060
+ // v0.11.8 (#102): normalize submissions before diffing so flat-shape
2061
+ // (observations + verdict) submissions emit meaningful artifact_diff
2062
+ // counts. Pre-0.11.8 (self.submission||{}).artifacts was undefined
2063
+ // for flat submissions; the diff returned all zeros even when
2064
+ // artifacts were present in observations.
2065
+ artifact_diff: diffArtifacts(
2066
+ normalizedArtifacts(self.submission, runner),
2067
+ normalizedArtifacts(other.submission, runner)
2068
+ ),
2069
+ signal_override_diff: diffSignalOverrides(
2070
+ normalizedSignalOverrides(self.submission, runner),
2071
+ normalizedSignalOverrides(other.submission, runner)
2072
+ ),
1941
2073
  }, pretty);
1942
2074
  return;
1943
2075
  }
@@ -1987,6 +2119,13 @@ function cmdAttest(runner, args, runOpts, pretty) {
1987
2119
  let formatRaw = args.format || "json";
1988
2120
  if (Array.isArray(formatRaw)) formatRaw = formatRaw[0];
1989
2121
  const format = formatRaw === "csaf-2.0" ? "csaf" : formatRaw;
2122
+ // v0.11.6 (#98): validate against accepted set. Pre-0.11.6 unknown
2123
+ // formats fell through to the default redacted JSON output, silently
2124
+ // accepting any value the operator passed.
2125
+ const VALID_EXPORT_FORMATS = ["json", "csaf", "csaf-2.0"];
2126
+ if (!VALID_EXPORT_FORMATS.includes(formatRaw)) {
2127
+ return emitError(`attest export: --format "${formatRaw}" not in accepted set ${JSON.stringify(VALID_EXPORT_FORMATS)}.`, null, pretty);
2128
+ }
1990
2129
  const redacted = attestations.map(a => ({
1991
2130
  session_id: a.session_id,
1992
2131
  playbook_id: a.playbook_id,
@@ -2032,6 +2171,36 @@ function cmdAttest(runner, args, runOpts, pretty) {
2032
2171
  return emitError(`attest: unknown subverb "${subverb}". Try export | verify | show.`, null, pretty);
2033
2172
  }
2034
2173
 
2174
+ /**
2175
+ * v0.11.8 (#102): extract normalized artifacts/signal_overrides from a stored
2176
+ * attestation submission. Flat-shape submissions store `observations` only;
2177
+ * nested submissions store `artifacts` + `signal_overrides`. Returning the
2178
+ * canonical nested view of both shapes lets `attest diff` produce meaningful
2179
+ * counts regardless of which shape the operator submitted.
2180
+ */
2181
+ function normalizedArtifacts(submission, runner) {
2182
+ if (!submission || typeof submission !== "object") return {};
2183
+ if (submission.artifacts) return submission.artifacts;
2184
+ if (submission.observations) {
2185
+ try {
2186
+ const norm = runner.normalizeSubmission({ observations: submission.observations }, { _meta: {}, phases: { look: { artifacts: [] } } });
2187
+ return norm.artifacts || {};
2188
+ } catch { return {}; }
2189
+ }
2190
+ return {};
2191
+ }
2192
+ function normalizedSignalOverrides(submission, runner) {
2193
+ if (!submission || typeof submission !== "object") return {};
2194
+ if (submission.signal_overrides) return submission.signal_overrides;
2195
+ if (submission.observations) {
2196
+ try {
2197
+ const norm = runner.normalizeSubmission({ observations: submission.observations }, { _meta: {}, phases: { look: { artifacts: [] } } });
2198
+ return norm.signal_overrides || {};
2199
+ } catch { return {}; }
2200
+ }
2201
+ return {};
2202
+ }
2203
+
2035
2204
  /**
2036
2205
  * Per-artifact diff between two submissions. Returns { added, removed, changed }
2037
2206
  * keyed by artifact id. Used by `attest diff` (bug #34 fix) so operators get
@@ -2043,15 +2212,22 @@ function diffArtifacts(a, b) {
2043
2212
  const out = { added: [], removed: [], changed: [], unchanged_count: 0 };
2044
2213
  for (const id of allIds) {
2045
2214
  const av = a[id], bv = b[id];
2046
- if (!av && bv) out.added.push({ id, captured: !!bv.captured, value_preview: previewValue(bv.value) });
2047
- else if (av && !bv) out.removed.push({ id, captured: !!av.captured, value_preview: previewValue(av.value) });
2048
- else if (JSON.stringify(av) !== JSON.stringify(bv)) {
2215
+ if (!av && bv) {
2216
+ out.added.push({ id, captured: !!bv.captured, value_preview: previewValue(bv.value) });
2217
+ } else if (av && !bv) {
2218
+ out.removed.push({ id, captured: !!av.captured, value_preview: previewValue(av.value) });
2219
+ } else if (av && bv && JSON.stringify(av) !== JSON.stringify(bv)) {
2049
2220
  out.changed.push({
2050
2221
  id,
2051
2222
  a_captured: !!av.captured, b_captured: !!bv.captured,
2052
2223
  a_value_preview: previewValue(av.value), b_value_preview: previewValue(bv.value),
2053
2224
  });
2054
- } else { out.unchanged_count++; }
2225
+ } else if (av && bv) {
2226
+ // v0.11.8 (#102): both sides have the entry AND they're identical →
2227
+ // unchanged. Pre-0.11.8 the unchanged path was unreachable because the
2228
+ // !av && bv guards short-circuited when both existed.
2229
+ out.unchanged_count++;
2230
+ }
2055
2231
  }
2056
2232
  return out;
2057
2233
  }
@@ -2452,6 +2628,31 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2452
2628
  },
2453
2629
  };
2454
2630
 
2631
+ // v0.11.6 (#97): --fix runs BEFORE the JSON early-return so `exceptd doctor
2632
+ // --fix --json` actually fixes (was a no-op pre-0.11.6). Re-runs the
2633
+ // signing check after fix so the returned JSON reflects the post-fix state.
2634
+ if (args.fix && checks.signing && !checks.signing.private_key_present) {
2635
+ process.stderr.write("[doctor --fix] generating Ed25519 keypair via `node lib/sign.js generate-keypair`...\n");
2636
+ const r = require("child_process").spawnSync(process.execPath, [path.join(PKG_ROOT, "lib", "sign.js"), "generate-keypair"], {
2637
+ stdio: ["ignore", "pipe", "pipe"], cwd: PKG_ROOT,
2638
+ });
2639
+ if (r.status === 0) {
2640
+ // Re-verify the private key is now present so the JSON output reflects
2641
+ // the fix.
2642
+ const keyPath = path.join(process.cwd(), ".keys", "private.pem");
2643
+ const fallback = path.join(PKG_ROOT, ".keys", "private.pem");
2644
+ const present = fs.existsSync(keyPath) || fs.existsSync(fallback);
2645
+ checks.signing = { ok: present, severity: present ? "info" : "warn", private_key_present: present, can_sign_attestations: present };
2646
+ out.checks = checks;
2647
+ out.summary.fix_applied = "ed25519_keypair_generated";
2648
+ process.stderr.write("[doctor --fix] keypair generated — re-checking signing status.\n");
2649
+ } else {
2650
+ out.summary.fix_attempted = "ed25519_keypair_generation_failed";
2651
+ out.summary.fix_exit_code = r.status;
2652
+ process.stderr.write(`[doctor --fix] generation failed (exit=${r.status}); run \`node lib/sign.js generate-keypair\` manually.\n`);
2653
+ }
2654
+ }
2655
+
2455
2656
  if (wantJson) {
2456
2657
  emit(out, indent);
2457
2658
  if (!allGreen) process.exitCode = 1;
@@ -2505,17 +2706,14 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2505
2706
  lines.push(`summary: ${errorList.length} fail / ${warnList.length} warn — fail: ${errorList.join(", ")}; warn: ${warnList.join(", ") || "none"}`);
2506
2707
  }
2507
2708
  process.stdout.write(lines.join("\n") + "\n");
2508
- // Bug #69 (v0.11.2): --fix mode for missing private key.
2509
- if (args.fix && checks.signing && !checks.signing.private_key_present) {
2510
- process.stdout.write("\n[doctor --fix] generating Ed25519 keypair via `node lib/sign.js generate-keypair`...\n");
2511
- const r = require("child_process").spawnSync(process.execPath, [path.join(PKG_ROOT, "lib", "sign.js"), "generate-keypair"], { stdio: "inherit", cwd: PKG_ROOT });
2512
- if (r.status === 0) {
2513
- process.stdout.write("[doctor --fix] keypair generated — re-run `exceptd doctor` to confirm.\n");
2514
- } else {
2515
- process.stdout.write(`[doctor --fix] generation failed (exit=${r.status}); run \`node lib/sign.js generate-keypair\` manually.\n`);
2516
- process.exitCode = 1;
2517
- return;
2518
- }
2709
+ // v0.11.6 (#97): --fix already ran above the JSON early-return. Echo the
2710
+ // applied/attempted state here for human readers.
2711
+ if (out.summary.fix_applied) {
2712
+ process.stdout.write(`\n[doctor --fix] ${out.summary.fix_applied} — re-run \`exceptd doctor\` to confirm.\n`);
2713
+ } else if (out.summary.fix_attempted) {
2714
+ 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`);
2715
+ process.exitCode = 1;
2716
+ return;
2519
2717
  }
2520
2718
  if (errorList.length > 0) process.exitCode = 1;
2521
2719
  // Warnings alone do NOT force exit 1 — CI gates use exit 0 to mean "ran
@@ -2563,7 +2761,21 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
2563
2761
  count: entries.length,
2564
2762
  filter: { playbook: args.playbook || null, since: args.since || null },
2565
2763
  roots_searched: [...seenRoots],
2566
- }, pretty);
2764
+ }, pretty, (obj) => {
2765
+ // v0.11.6 (#95) human renderer for attest list: one row per session.
2766
+ const lines = [`attest list — ${obj.count} attestation(s)`];
2767
+ if (obj.count === 0) {
2768
+ lines.push(` (no attestations under ${obj.roots_searched.join(' or ')})`);
2769
+ return lines.join("\n");
2770
+ }
2771
+ lines.push(` ${"session-id".padEnd(20)} ${"playbook".padEnd(16)} ${"captured-at".padEnd(20)} evidence-hash`);
2772
+ lines.push(` ${"-".repeat(20)} ${"-".repeat(16)} ${"-".repeat(20)} ${"-".repeat(20)}`);
2773
+ for (const e of obj.attestations.slice(0, 50)) {
2774
+ lines.push(` ${(e.session_id || "?").padEnd(20)} ${(e.playbook_id || "?").padEnd(16)} ${(e.captured_at || "").slice(0, 19).padEnd(20)} ${e.evidence_hash || ""}`);
2775
+ }
2776
+ if (obj.count > 50) lines.push(` … and ${obj.count - 50} more (use --json for full list)`);
2777
+ return lines.join("\n");
2778
+ });
2567
2779
  }
2568
2780
 
2569
2781
  // ---------------------------------------------------------------------------
@@ -2683,20 +2895,28 @@ function cmdAiRun(runner, args, runOpts, pretty) {
2683
2895
  process.stderr.write((pretty ? JSON.stringify(result || {}, null, 2) : JSON.stringify(result || {})) + "\n");
2684
2896
  process.exit(1);
2685
2897
  }
2898
+ // v0.11.8 (#101): unify ai-run --no-stream shape with `run`. Pre-0.11.8
2899
+ // ai-run flattened phases to top-level (`govern`, `direct`, `look`, ...),
2900
+ // while `run` nested them under `phases.*`. Operators writing JSONPath
2901
+ // queries had to know which verb produced the payload. Now both share
2902
+ // `{ok, playbook_id, directive_id, session_id, evidence_hash, phases: {...}}`.
2686
2903
  emit({
2904
+ ok: result.ok !== false,
2687
2905
  verb: "ai-run",
2688
2906
  mode: "no-stream",
2689
2907
  playbook_id: playbookId,
2690
2908
  directive_id: directiveId,
2691
- govern: governEvent,
2692
- direct: directEvent,
2693
- look: lookEvent,
2694
- detect: result.phases?.detect || null,
2695
- analyze: result.phases?.analyze || null,
2696
- validate: result.phases?.validate || null,
2697
- close: result.phases?.close || null,
2698
2909
  session_id: result.session_id,
2699
2910
  evidence_hash: result.evidence_hash,
2911
+ phases: {
2912
+ govern: governEvent,
2913
+ direct: directEvent,
2914
+ look: lookEvent,
2915
+ detect: result.phases?.detect || null,
2916
+ analyze: result.phases?.analyze || null,
2917
+ validate: result.phases?.validate || null,
2918
+ close: result.phases?.close || null,
2919
+ },
2700
2920
  }, pretty);
2701
2921
  return;
2702
2922
  }
@@ -3049,19 +3269,31 @@ function cmdCi(runner, args, runOpts, pretty) {
3049
3269
  continue;
3050
3270
  }
3051
3271
  const cls = result.phases?.detect?.classification;
3272
+ const rwepBase = result.phases?.analyze?.rwep?.base ?? 0;
3052
3273
  const rwepAdj = result.phases?.analyze?.rwep?.adjusted ?? 0;
3053
3274
  const cap = maxRwep !== null
3054
3275
  ? maxRwep
3055
3276
  : (result.phases?.analyze?.rwep?.threshold?.escalate ?? 90);
3056
3277
  const clockStarted = (result.phases?.close?.notification_actions || [])
3057
3278
  .some(n => n && n.clock_started_at != null);
3279
+
3058
3280
  if (cls === "detected") {
3059
3281
  fail = true;
3060
3282
  failReasons.push(`${id}: classification=detected`);
3061
3283
  }
3062
- if (cls !== "not_detected" && cls !== "clean" && rwepAdj >= cap) {
3284
+ // v0.11.8 (#103): only count RWEP against the cap when the operator's
3285
+ // signals actually moved the score, OR classification reached "detected".
3286
+ // Pre-0.11.8 a fresh `ci --scope code` run with NO operator evidence
3287
+ // failed because catalog-baseline RWEP (e.g. 90 for KEV-listed kernel
3288
+ // CVEs) exceeded the default cap (80). That penalized inconclusive runs
3289
+ // for catalogue facts the operator hadn't yet weighed in on. Now: only
3290
+ // RWEP DELTA (adjusted - base) counts against the cap on inconclusive
3291
+ // classifications. Detected runs still fail on absolute RWEP.
3292
+ if (cls === "detected" && rwepAdj >= cap) {
3293
+ // Already failed above; this branch documents the rationale.
3294
+ } else if (cls === "inconclusive" && rwepAdj - rwepBase >= cap) {
3063
3295
  fail = true;
3064
- failReasons.push(`${id}: rwep=${rwepAdj} >= cap=${cap} (classification=${cls})`);
3296
+ failReasons.push(`${id}: rwep_delta=${rwepAdj - rwepBase} >= cap=${cap} (classification=inconclusive; operator evidence raised the score)`);
3065
3297
  }
3066
3298
  if (blockOnClock && clockStarted) {
3067
3299
  fail = true;
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-12T17:00:14.346Z",
3
+ "generated_at": "2026-05-12T18:19:14.173Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "da39bcf4cd05d571b5d76a009ede9ee721aa783c07764636e0f0a0bd4634a99a",
7
+ "manifest.json": "478e454559d56c02a04fbfc0fe83250c87d4ba6c9f5a479fa8ad6ae6311f9ec0",
8
8
  "data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
9
9
  "data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
10
10
  "data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
package/keys/public.pem CHANGED
@@ -1,3 +1,3 @@
1
1
  -----BEGIN PUBLIC KEY-----
2
- MCowBQYDK2VwAyEAh++7P6Bd/u2vy1xF05jGxrN/VJqtFennBw5JZy8kx6M=
2
+ MCowBQYDK2VwAyEARO2X9Ni3eO7BBm4kjwQjzQvHWtMxXa9RJZ+F1fCWMLQ=
3
3
  -----END PUBLIC KEY-----
@@ -790,21 +790,37 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
790
790
  notes: [{ category: 'description', text: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? ' / deterministic' : ''}) in playbook ${playbook._meta.id}.` }],
791
791
  remediations: [{ category: 'mitigation', details: validate.selected_remediation?.description || `Consult playbook brief: exceptd brief ${playbook._meta.id}.` }],
792
792
  }));
793
+ // v0.11.6 (#91): framework_gap_mapping → CSAF vulnerabilities. Each gap
794
+ // becomes a vulnerability keyed by the framework + control, with the
795
+ // gap text as the description and the required_control as the remediation.
796
+ const gapVulns = (analyze.framework_gap_mapping || []).map((g, idx) => ({
797
+ ids: [{ system_name: 'exceptd-framework-gap', text: `${g.framework}:${g.claimed_control || `gap-${idx}`}` }],
798
+ notes: [
799
+ { category: 'description', text: g.actual_gap || `Framework gap in ${g.framework} ${g.claimed_control || ''}` },
800
+ { category: 'general', text: g.claimed_control ? `Claimed control: ${g.claimed_control}` : null },
801
+ ].filter(n => n.text),
802
+ remediations: g.required_control ? [{ category: 'mitigation', details: g.required_control }] : [],
803
+ }));
804
+ const now = new Date().toISOString();
793
805
  return {
794
806
  document: {
795
807
  category: 'csaf_security_advisory',
796
808
  csaf_version: '2.0',
797
809
  publisher: { category: 'vendor', name: 'exceptd', namespace: 'https://exceptd.com' },
798
- title: `exceptd finding: ${playbook.domain.name} (${analyze.matched_cves.length} CVE(s), ${indicatorHits.length} indicator hit(s))`,
810
+ title: `exceptd finding: ${playbook.domain.name} (${analyze.matched_cves.length} CVE(s), ${indicatorHits.length} indicator hit(s), ${(analyze.framework_gap_mapping || []).length} framework gap(s))`,
799
811
  tracking: {
800
812
  id: `exceptd-${playbook._meta.id}-${Date.now()}`,
801
813
  status: 'final',
802
814
  version: playbook._meta.version,
803
- initial_release_date: new Date().toISOString(),
804
- revision_history: [{ number: '1', date: new Date().toISOString(), summary: 'Initial finding emission' }]
815
+ initial_release_date: now,
816
+ // v0.11.6 (#92): CSAF 2.0 §3.2.1.12 requires current_release_date
817
+ // non-null. Pre-0.11.6 we only set initial_release_date and
818
+ // downstream validators rejected the bundle.
819
+ current_release_date: now,
820
+ revision_history: [{ number: '1', date: now, summary: 'Initial finding emission' }]
805
821
  }
806
822
  },
807
- vulnerabilities: [...cveVulns, ...indicatorVulns],
823
+ vulnerabilities: [...cveVulns, ...indicatorVulns, ...gapVulns],
808
824
  exceptd_extension: {
809
825
  classification: analyze._detect_classification,
810
826
  rwep: analyze.rwep,
@@ -863,13 +879,25 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
863
879
  fullDescription: { text: `Indicator from playbook ${playbook._meta.id}. Type: ${i.type}. Confidence: ${i.confidence}.` },
864
880
  defaultConfiguration: { level: i.deterministic ? 'error' : (i.confidence === 'high' ? 'warning' : 'note') },
865
881
  }));
882
+ // v0.11.6 (#93): SARIF spec §3.27.3 — every referenced ruleId SHOULD have
883
+ // a corresponding rule definition in tool.driver.rules. Pre-0.11.6 we
884
+ // referenced framework-gap-N ids without defining them; GitHub Code
885
+ // Scanning + VS Code SARIF Viewer + Azure DevOps would warn or fail to
886
+ // display rule context. Now we emit one rule per framework gap.
887
+ const gapRules = (analyze.framework_gap_mapping || []).map((g, idx) => ({
888
+ id: `framework-gap-${idx}`,
889
+ shortDescription: { text: `${g.framework}: ${g.claimed_control || `gap-${idx}`}` },
890
+ fullDescription: { text: g.actual_gap || `Framework gap in ${g.framework}` },
891
+ defaultConfiguration: { level: 'note' },
892
+ help: g.required_control ? { text: `Required control: ${g.required_control}` } : undefined,
893
+ }));
866
894
  return {
867
895
  $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
868
896
  version: '2.1.0',
869
897
  runs: [{
870
898
  tool: { driver: {
871
899
  name: 'exceptd', version: playbook._meta.version, informationUri: 'https://exceptd.com',
872
- rules: [...cveRules, ...indicatorRules],
900
+ rules: [...cveRules, ...indicatorRules, ...gapRules],
873
901
  } },
874
902
  results: [...cveResults, ...indicatorResults, ...gapResults],
875
903
  invocations: [{ executionSuccessful: true, properties: {
@@ -900,13 +928,23 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
900
928
  action_statement: validate.selected_remediation?.description || `Run \`exceptd brief ${playbook._meta.id}\` for context.`,
901
929
  impact_statement: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? '/deterministic' : ''}) in playbook ${playbook._meta.id}.`,
902
930
  }));
931
+ // v0.11.6 (#91): framework gaps → OpenVEX statements. Each gap becomes
932
+ // a statement with a pseudo-CVE id under the exceptd:framework-gap
933
+ // namespace so VEX downstreams ingest them cleanly.
934
+ const gapStatements = (analyze.framework_gap_mapping || []).map((g, idx) => ({
935
+ vulnerability: { '@id': `exceptd:framework-gap:${g.framework}:${g.claimed_control || idx}`, name: `${g.framework} ${g.claimed_control || `gap-${idx}`}` },
936
+ status: 'under_investigation',
937
+ timestamp: issued,
938
+ action_statement: g.required_control || null,
939
+ impact_statement: g.actual_gap || `Framework gap in ${g.framework}.`,
940
+ }));
903
941
  return {
904
942
  '@context': 'https://openvex.dev/ns/v0.2.0',
905
943
  '@id': `https://exceptd.com/vex/${playbook._meta.id}/${Date.now()}`,
906
944
  author: 'exceptd',
907
945
  timestamp: issued,
908
946
  version: 1,
909
- statements: [...cveStatements, ...indicatorStatements],
947
+ statements: [...cveStatements, ...indicatorStatements, ...gapStatements],
910
948
  };
911
949
  }
912
950
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
3
- "_generated_at": "2026-05-12T16:58:59.224Z",
3
+ "_generated_at": "2026-05-12T18:18:22.013Z",
4
4
  "atlas_version": "5.1.0",
5
5
  "skill_count": 38,
6
6
  "skills": [
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.11.5",
3
+ "version": "0.11.8",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -51,8 +51,8 @@
51
51
  "RFC-7296"
52
52
  ],
53
53
  "last_threat_review": "2026-05-01",
54
- "signature": "WprHkO1KOjQtCBj6/EJghBTNyNKJhn7O2HDbAQZPi5jn4flwHpSrtP8LC15a4Unoh+xiIIgGhvTHZIQFHGMpBQ==",
55
- "signed_at": "2026-05-12T16:58:58.659Z",
54
+ "signature": "Xk593pj7my6wPJbQBE47khpIUrPsp6N1lW7cE2T/VPPF5T+8C1yGKc9B8VphD7Q08yWFcbwF6HoWpA/+4uG9DA==",
55
+ "signed_at": "2026-05-12T18:18:21.605Z",
56
56
  "cwe_refs": [
57
57
  "CWE-125",
58
58
  "CWE-362",
@@ -115,8 +115,8 @@
115
115
  "SOC2-CC6-logical-access"
116
116
  ],
117
117
  "last_threat_review": "2026-05-01",
118
- "signature": "fg20bOXGRkPUdLmegeXpTM4hnzl/ArgcVc88rItZN5DdsnFnzPgUU1PwCI82zooyj2GfxJHYjxNkq5qd2zNPBg==",
119
- "signed_at": "2026-05-12T16:58:58.661Z",
118
+ "signature": "nOgUu+LK9fy6ASTCoRGtx3ttgjZCl7WIkKu2wu06JEKVSpL2cKU3ex2tmVAvv11LBmpTH+b/0zvqXlzcxzHnCw==",
119
+ "signed_at": "2026-05-12T18:18:21.607Z",
120
120
  "cwe_refs": [
121
121
  "CWE-1039",
122
122
  "CWE-1426",
@@ -178,8 +178,8 @@
178
178
  "RFC-9700"
179
179
  ],
180
180
  "last_threat_review": "2026-05-01",
181
- "signature": "6JuSzkSSFzFHEZ3ANzqjtIbKPOkwJeKhQ+8WAPB4+dTRvDSeg46n3D88XfGaNd2z7pmg/i8p9ZoImQcHFS4BCg==",
182
- "signed_at": "2026-05-12T16:58:58.662Z",
181
+ "signature": "7FH1J9PlOyvcRCzRmggmenX9fIR0pi/veXihb3TeStcq1Rpuz1KHdOcJLqA9su4t2goYukKKCXHV6hx8hzplAA==",
182
+ "signed_at": "2026-05-12T18:18:21.608Z",
183
183
  "cwe_refs": [
184
184
  "CWE-22",
185
185
  "CWE-345",
@@ -224,8 +224,8 @@
224
224
  "attack_refs": [],
225
225
  "framework_gaps": [],
226
226
  "last_threat_review": "2026-05-01",
227
- "signature": "PYSw9abiYfW+y7IkY8udJG5LSds2a4rMimlw3rrdD0zE3vunEeV/y7oTmDD4o83OqHSCKNzF/7vMhvd/noqICQ==",
228
- "signed_at": "2026-05-12T16:58:58.662Z"
227
+ "signature": "FqTRjHfEgw56pyHnyWzNtnhzDMEePBtmuamtW/iyX+h4yqbvP4Fyr7NRjRs3EgqT4j7oHuEZhV9Jt6ZTBgN4AA==",
228
+ "signed_at": "2026-05-12T18:18:21.608Z"
229
229
  },
230
230
  {
231
231
  "name": "compliance-theater",
@@ -255,8 +255,8 @@
255
255
  "CMMC-2.0-Level-2"
256
256
  ],
257
257
  "last_threat_review": "2026-05-01",
258
- "signature": "BMFmmJYP3HsHIjUqnhw8E3MiMGZJsI/eDq51we+nxUicZ8nFUQT9DhmRntAqOs6BUnsfiQNNLc/rrsNh8yg1CQ==",
259
- "signed_at": "2026-05-12T16:58:58.663Z"
258
+ "signature": "3fN4yotiIIq76PVTHwozCu28TzDZvWule6vX8SXUT3XXbIBSuvAO0M/euvc3pw3TdZ2UNf78dI18lOCNdJ0aAg==",
259
+ "signed_at": "2026-05-12T18:18:21.609Z"
260
260
  },
261
261
  {
262
262
  "name": "exploit-scoring",
@@ -284,8 +284,8 @@
284
284
  "CIS-Controls-v8-Control7"
285
285
  ],
286
286
  "last_threat_review": "2026-05-01",
287
- "signature": "VGPyDwy5BRlpn1lZthhPB6ytb4ZcU2j0KtCZbaMkyLdMugQJtK2yEuwrsDH4yEtAhTB6/A4B3eSygJckum49Ag==",
288
- "signed_at": "2026-05-12T16:58:58.663Z"
287
+ "signature": "yZfpk4lQMRXegj2ADWjMmZTchUN6Lxpv587O/0JMzbNkXQtD6FrSAQOBWjx8S7uQ/sTntxgGN7aQQDLxL9RWAA==",
288
+ "signed_at": "2026-05-12T18:18:21.609Z"
289
289
  },
290
290
  {
291
291
  "name": "rag-pipeline-security",
@@ -321,8 +321,8 @@
321
321
  "OWASP-LLM-Top-10-2025-LLM08"
322
322
  ],
323
323
  "last_threat_review": "2026-05-01",
324
- "signature": "XkFGpsNnXBVslkQ48usEu9l1LjPiV2ppW+M4B63zXFBP2Puh52qYCffEPjUHYhoO5bjgTM7yCbK8XF/Dzk5wBw==",
325
- "signed_at": "2026-05-12T16:58:58.663Z",
324
+ "signature": "ABHkoqee67KdUyDZ3bvF+/DNxjGhPR/ehT6pfOnmUIMmkcQFHpZ0OUVXKiFUANaLgKLP1vg0VEmHOoxpNA3vAA==",
325
+ "signed_at": "2026-05-12T18:18:21.609Z",
326
326
  "cwe_refs": [
327
327
  "CWE-1395",
328
328
  "CWE-1426"
@@ -378,8 +378,8 @@
378
378
  "RFC-9000"
379
379
  ],
380
380
  "last_threat_review": "2026-05-01",
381
- "signature": "1Xqy7Kxxy6GpTvuYJPdllPzVDRFxb7N6AuxKuoaO4v91CiZLmiXt0sTIWImKJ3p9Eup6rJNDdsY71dolFhHNBA==",
382
- "signed_at": "2026-05-12T16:58:58.664Z",
381
+ "signature": "+Nd/2tgBnW+mEGX84QvkgR2To2J7kA+lB63BsADDKeCXeebFv6Vo9H1P4vyUkKHfe4fP0ndpy3agIZcUO/e/Dg==",
382
+ "signed_at": "2026-05-12T18:18:21.609Z",
383
383
  "d3fend_refs": [
384
384
  "D3-CA",
385
385
  "D3-CSPP",
@@ -413,8 +413,8 @@
413
413
  "attack_refs": [],
414
414
  "framework_gaps": [],
415
415
  "last_threat_review": "2026-05-01",
416
- "signature": "QNLOmAL54S/Cmk4cdO4L2BCGkqZ/FgY4UBsKWtg/EEW+YXF5ev+a8XsUT8q5veuUa2VYcYna7rD1iAnE+2PDBA==",
417
- "signed_at": "2026-05-12T16:58:58.664Z",
416
+ "signature": "VMNGFvowXLbBjZp5nvWloKkqyqHKhnSzbVRU3gX9quOZJHH56w2M4id+oDsXIjR0CfRRb7eXl/so0Hq4xLBuBQ==",
417
+ "signed_at": "2026-05-12T18:18:21.610Z",
418
418
  "cwe_refs": [
419
419
  "CWE-1188"
420
420
  ]
@@ -441,8 +441,8 @@
441
441
  "attack_refs": [],
442
442
  "framework_gaps": [],
443
443
  "last_threat_review": "2026-05-01",
444
- "signature": "aFHq4cSl3CKchnVITxx+BrAEWD33WtFFJoQtwAug5g9R3/3ABtjaXYGVQaZcdcG1AIZkMoGSPywgLQWDY7ZDCw==",
445
- "signed_at": "2026-05-12T16:58:58.664Z"
444
+ "signature": "5MaJs7gPCuFlK4oAttLulAPOA1noeV+xD/UqVWaVyRedXZgebBGKjnlE2t1qmTugvxlNIfeAnBZapk+Wz3VAAg==",
445
+ "signed_at": "2026-05-12T18:18:21.610Z"
446
446
  },
447
447
  {
448
448
  "name": "global-grc",
@@ -473,8 +473,8 @@
473
473
  "attack_refs": [],
474
474
  "framework_gaps": [],
475
475
  "last_threat_review": "2026-05-01",
476
- "signature": "viCTUWdy6euvd2KTAo6sLvarK/FZkDtYGocxBt0H+fY94kLQGW8K5cSpqIWdUF5NUytSHBCiG4YcSze8P9Z/BQ==",
477
- "signed_at": "2026-05-12T16:58:58.665Z"
476
+ "signature": "S/YXUpI/mcG2FpdUTgMsccWBtTaR5A4Ph4QFQw31S9w9Hn/z3sOFHLkb1B5YSwlg+mMOtSIxMdet1eLGSZkTDg==",
477
+ "signed_at": "2026-05-12T18:18:21.610Z"
478
478
  },
479
479
  {
480
480
  "name": "zeroday-gap-learn",
@@ -500,8 +500,8 @@
500
500
  "attack_refs": [],
501
501
  "framework_gaps": [],
502
502
  "last_threat_review": "2026-05-01",
503
- "signature": "6PkUaHQi3Hxuqq/Jp4GYckvfqVEofmeT87NUH0T+pwyjlc+xZkoqNPn65f7ldciEPL86JIPi3/dDTKQbIFFBCw==",
504
- "signed_at": "2026-05-12T16:58:58.665Z"
503
+ "signature": "AKS+JsmhhBtytY2eIMuydjkZOYprWCmQ+RqxyxcVG9XcEI29ZSM/JbVIINQHozFl7OPPrOu1ouiTnk7LOJ86Bg==",
504
+ "signed_at": "2026-05-12T18:18:21.611Z"
505
505
  },
506
506
  {
507
507
  "name": "pqc-first",
@@ -552,8 +552,8 @@
552
552
  "CRQC timeline estimate changes"
553
553
  ],
554
554
  "last_threat_review": "2026-05-01",
555
- "signature": "ZenFTEzWx+DzrSXlNXhbZ70vOdJSXfrnKkAwqMlBf5nlDf38V1/hG4XCKj43snQXWr4mVJOX6ilqFLTYNIjnBw==",
556
- "signed_at": "2026-05-12T16:58:58.665Z",
555
+ "signature": "oEkK5bLS/G5RIHnxlNFJYdzhTJbKZnkJv+W4iS9UJ/uszZHgZGoxygELPc4kn3FowV5eE988SQYG4WKlXtNzCg==",
556
+ "signed_at": "2026-05-12T18:18:21.611Z",
557
557
  "cwe_refs": [
558
558
  "CWE-327"
559
559
  ],
@@ -599,8 +599,8 @@
599
599
  "Framework publication updates"
600
600
  ],
601
601
  "last_threat_review": "2026-05-01",
602
- "signature": "ih0vpd2v2zS31JSJv7SnABoya8JlJdrXZXx4rBnrsV3Assj+dbjAP0pQ1HMT/5RX8yTTswRQsg0bJV3qmbJ3Bw==",
603
- "signed_at": "2026-05-12T16:58:58.666Z"
602
+ "signature": "nPV6YTo1rsNH49qUnZpfoNLEQZXuLNyV05QMUOgXKHYeVDjotYpWhLgyVXlRhjV/fStiA2sWQ0MOnEJ4FBIfDg==",
603
+ "signed_at": "2026-05-12T18:18:21.612Z"
604
604
  },
605
605
  {
606
606
  "name": "security-maturity-tiers",
@@ -636,8 +636,8 @@
636
636
  "PQC tooling maturity shifting overkill to practical"
637
637
  ],
638
638
  "last_threat_review": "2026-05-01",
639
- "signature": "Lv8dHiwIqUbNsywCCB/+pYWGF+MHCvxVn1IAvR7Cnif5fy0sICv0N4SVsSb621qAAkHNshpfxqwuhbuQnE1TBA==",
640
- "signed_at": "2026-05-12T16:58:58.666Z",
639
+ "signature": "7rirSEONz6O9Yyf46eTyuwkGizCj9FRcNHe5p7Qz6nhJoZQRW5FwW7n9opL0WlbIw8FDBYn1f22zgNUV87L5AQ==",
640
+ "signed_at": "2026-05-12T18:18:21.612Z",
641
641
  "cwe_refs": [
642
642
  "CWE-1188"
643
643
  ]
@@ -671,8 +671,8 @@
671
671
  "attack_refs": [],
672
672
  "framework_gaps": [],
673
673
  "last_threat_review": "2026-05-11",
674
- "signature": "BS+wrL28HHYhBpe+v84VLoq9KPBXu6alfG968katfGIoLNYQueaHP931bRmlkrjfeb6qbDf067GWdPEh7nroAw==",
675
- "signed_at": "2026-05-12T16:58:58.666Z"
674
+ "signature": "+evehnd2wSBb8uMTlTr5/aTN4bfLjsKzZJk/+OMLMOJrjCt+OuMU7EQC6xMUGeSc4cPEGajghDvq3xVaacV2Dw==",
675
+ "signed_at": "2026-05-12T18:18:21.612Z"
676
676
  },
677
677
  {
678
678
  "name": "attack-surface-pentest",
@@ -742,8 +742,8 @@
742
742
  "OWASP WSTG v5.x AI/MCP test cases (currently in working-group draft)",
743
743
  "PTES revision incorporating AI-surface enumeration"
744
744
  ],
745
- "signature": "vLhIYT/CC3IzxMRa+UPeqGSZTvthuwUeTMGNFMm37+TaEk0TtfwPrPyrBJLHw4W6Wt7+pufjHs46X3nTgzoRAg==",
746
- "signed_at": "2026-05-12T16:58:58.667Z"
745
+ "signature": "KHOXxloAYf7xqXjm2BaL3HVAZOmb7rMiMh20H/oaIkjN0WD1CnKCrRGPJn867uSFhCh/timkXolaiqD1L/h8Dg==",
746
+ "signed_at": "2026-05-12T18:18:21.612Z"
747
747
  },
748
748
  {
749
749
  "name": "fuzz-testing-strategy",
@@ -802,8 +802,8 @@
802
802
  "syzkaller eBPF and io_uring surface expansion as new kernel attack surfaces ship",
803
803
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
804
804
  ],
805
- "signature": "TOcQLy/427cuf0Lw90J7A0oIeuhUmf9NXb6tOUS5K3SazCKTJujPgYSVAPZOYf1zZrRAY/aq0iqELd5cLyk5DA==",
806
- "signed_at": "2026-05-12T16:58:58.667Z"
805
+ "signature": "+ELdD+1AY5DymBitH7wU65CS60NY1nDoLowJAFn7cE5Gr/5jy9BTkyxsm7PEXaSlXWMOkTf/HQ+uyzyxUVD/Bw==",
806
+ "signed_at": "2026-05-12T18:18:21.613Z"
807
807
  },
808
808
  {
809
809
  "name": "dlp-gap-analysis",
@@ -877,8 +877,8 @@
877
877
  "MCP gateway / proxy standardisation (Anthropic enterprise MCP gateway, Portkey MCP) — tool-call argument inspection is the missing primary control",
878
878
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
879
879
  ],
880
- "signature": "u4IN7escQa5V+OgdtaJXLdvhmNiGZsdmGOvebTLZ30WoImT+WiksvaqSa0POGdbr6HzFkALe2RrZEH9Tr0U6Dg==",
881
- "signed_at": "2026-05-12T16:58:58.667Z"
880
+ "signature": "8tFAhXAS8zZN3SUOdn+ZIu7lQ48JMOyBQ8SaObR3L/fDyFmDhufqleY2VzI3yigqlT/D4Y8FYxZHKmzXiALjDw==",
881
+ "signed_at": "2026-05-12T18:18:21.613Z"
882
882
  },
883
883
  {
884
884
  "name": "supply-chain-integrity",
@@ -954,8 +954,8 @@
954
954
  "EU CRA (Regulation 2024/2847) — implementing acts for technical documentation and SBOM submission expected through 2027",
955
955
  "OpenSSF model-signing — emerging Sigstore-based signing standard for ML model weights; track for production adoption"
956
956
  ],
957
- "signature": "eTGQJ3gnG24WggfwuFNNIFOWV/ttPxTa3pvx9OH28m5KDS1a4ZmOR7K8y01wk/su8bH0ClYYRfoBfKQOtRswAg==",
958
- "signed_at": "2026-05-12T16:58:58.667Z"
957
+ "signature": "8xlk5ZfTKVYqTE2+ifkjTBu/RPqs4MIvX7SpOHl22YDHi7nzJ1ywPhSNYJzoPdPV4AUuWG518EldQJsEIuyuAA==",
958
+ "signed_at": "2026-05-12T18:18:21.613Z"
959
959
  },
960
960
  {
961
961
  "name": "defensive-countermeasure-mapping",
@@ -1011,8 +1011,8 @@
1011
1011
  "D3-SCP"
1012
1012
  ],
1013
1013
  "last_threat_review": "2026-05-11",
1014
- "signature": "q7gFLPoqf/8bqATR6gt/nj0EoyUOlfzi+bZ0bT3pC9KW7O6M/ji9fT+AXSGNp6PKd+70ACb3mkMGmWgjLpQXCg==",
1015
- "signed_at": "2026-05-12T16:58:58.668Z"
1014
+ "signature": "AMdLkDx/e3ESI4NAnJhhcaas+Ru8VjrSn6v6RBbmmzoLCGo/vFxGraa1p/qF9udhVG+DdkbwHfbfKK5Im19KDw==",
1015
+ "signed_at": "2026-05-12T18:18:21.613Z"
1016
1016
  },
1017
1017
  {
1018
1018
  "name": "identity-assurance",
@@ -1078,8 +1078,8 @@
1078
1078
  ],
1079
1079
  "d3fend_refs": [],
1080
1080
  "last_threat_review": "2026-05-11",
1081
- "signature": "pX8rhrrzuyG3iRrPORLqTZAjzGdWK/bKPUGJG5WHSZcv4LB0kQXOit4sHG0exdXxI6HY8jyX67QY4r5vEHHACw==",
1082
- "signed_at": "2026-05-12T16:58:58.668Z"
1081
+ "signature": "pSMHKkyWoZvRIuVtN7Vue51sP5MIy9lSaQa2YSAMhxjptx81cUnPt3S11/Tb9Ea1/eluMNQ+5F25eF2njr4mBQ==",
1082
+ "signed_at": "2026-05-12T18:18:21.614Z"
1083
1083
  },
1084
1084
  {
1085
1085
  "name": "ot-ics-security",
@@ -1134,8 +1134,8 @@
1134
1134
  ],
1135
1135
  "d3fend_refs": [],
1136
1136
  "last_threat_review": "2026-05-11",
1137
- "signature": "ypb8kNZQRdyu5mWeveB7sjCjNKXS1yXvjDJv88muzwhOs/a4Fu/Gb532js5NKyy+eCw/emrphpTZaL8R9a2lBA==",
1138
- "signed_at": "2026-05-12T16:58:58.668Z"
1137
+ "signature": "qjky+ZTX1DP7uRRMQZq7S7P9/uaJEoB1dy4RZ1l37Q4OO3k2ryfL+7o0Cgm/piuafJfH+dqUeNCRrVefj4r8Dw==",
1138
+ "signed_at": "2026-05-12T18:18:21.614Z"
1139
1139
  },
1140
1140
  {
1141
1141
  "name": "coordinated-vuln-disclosure",
@@ -1186,8 +1186,8 @@
1186
1186
  "UK NCSC Vulnerability Disclosure Toolkit revisions and AU ISM CVD guidance updates",
1187
1187
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1188
1188
  ],
1189
- "signature": "346Lt+277ycRNsyAOGwLSONi4awgxKy3hP9G+BWjwaa8ySmTeqbYsbyyhtxjeohk9bV2SF+Hl2q4JdSvc/2qCQ==",
1190
- "signed_at": "2026-05-12T16:58:58.668Z"
1189
+ "signature": "F86Zl/I+dBzHYRUuGWsjDQI2F/I/vhzwZUFMqhNfKUzRbMf6mafOX2APCPYTp3eP1DvvvfL3Yc0hb1R5Q4nOAg==",
1190
+ "signed_at": "2026-05-12T18:18:21.614Z"
1191
1191
  },
1192
1192
  {
1193
1193
  "name": "threat-modeling-methodology",
@@ -1236,8 +1236,8 @@
1236
1236
  "LINDDUN-GO and LINDDUN-PRO updates incorporating LLM privacy threats",
1237
1237
  "PASTA v2 updates incorporating AI/ML application threats"
1238
1238
  ],
1239
- "signature": "ewTvG5vu3ngFHyXgBur5vSKDFQsOZx0x79djGMricl7LCvQf5//OG6LZKXa+AOuEq58prRS+HgzrFA1DiTfeCQ==",
1240
- "signed_at": "2026-05-12T16:58:58.669Z"
1239
+ "signature": "D/4d5NcJScNH58ADXsSrVzTmLSWZpUZTdyhtDkJlC0twSMNczOiDsXgYFitBaZgGdv5nVd00viR45mNrsaZ4BQ==",
1240
+ "signed_at": "2026-05-12T18:18:21.615Z"
1241
1241
  },
1242
1242
  {
1243
1243
  "name": "webapp-security",
@@ -1310,8 +1310,8 @@
1310
1310
  ],
1311
1311
  "d3fend_refs": [],
1312
1312
  "last_threat_review": "2026-05-11",
1313
- "signature": "ZHjbKu0Em92Kimr2esL1g93mf9TmcsChBhVEMWf/lFrjeLcg8nyHEIcDstIZ3FWYgc6MQNHnc3Rup3Xp/Za1Cw==",
1314
- "signed_at": "2026-05-12T16:58:58.669Z"
1313
+ "signature": "UOXaUtpcFjXyDQ70z2PaGu6K3pABtXp+7YzO6eGVGpN1CxXpPq/xW/CnTng6B7wk9WSsqD0OORBJp4VCjiVfAQ==",
1314
+ "signed_at": "2026-05-12T18:18:21.615Z"
1315
1315
  },
1316
1316
  {
1317
1317
  "name": "ai-risk-management",
@@ -1360,8 +1360,8 @@
1360
1360
  ],
1361
1361
  "d3fend_refs": [],
1362
1362
  "last_threat_review": "2026-05-11",
1363
- "signature": "1KRxjCbAX0Rs5NTOioi1w/f1SOzDQrtRoXjTDtzEwJ+d1QzFf9cqmBlp0uXmGpL0bzEaHWIctjigSychmoL2Dw==",
1364
- "signed_at": "2026-05-12T16:58:58.670Z"
1363
+ "signature": "IVKygsrFjiM64fQVbd2PT6jDjs6fm5nKwJSqGfK53gG0S9wdHC4QYuh+LWlI/2ftvIKjjedLQ6FRyTrqpDEuDw==",
1364
+ "signed_at": "2026-05-12T18:18:21.615Z"
1365
1365
  },
1366
1366
  {
1367
1367
  "name": "sector-healthcare",
@@ -1420,8 +1420,8 @@
1420
1420
  ],
1421
1421
  "d3fend_refs": [],
1422
1422
  "last_threat_review": "2026-05-11",
1423
- "signature": "eiajFh7w7d4g+/crGalTtw9Qsu0deVsdHkdthZSy595ifGmgu0zaFD8usKThbPhOdUCCclTYkZYz5GalQmkhCw==",
1424
- "signed_at": "2026-05-12T16:58:58.670Z"
1423
+ "signature": "P+CdSu8ZJCNUU4nTa09Voh2PcYF3y/AFJn4v7cjVIGo9FbbqO7MwvGN7cJ+aSRs2/3NMUXX4eupcODslxYyJDw==",
1424
+ "signed_at": "2026-05-12T18:18:21.616Z"
1425
1425
  },
1426
1426
  {
1427
1427
  "name": "sector-financial",
@@ -1501,8 +1501,8 @@
1501
1501
  "OSFI B-13 (Technology and Cyber Risk Management) post-2024 examination findings",
1502
1502
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1503
1503
  ],
1504
- "signature": "iSZR/fYESQVyjkcqj+O+yzU0BQfaELH5s7WizzUTWvDPDTD2ZyOnZTT1r/Zfx2l4mbPmVeFGWdYnnVFTk/i3Aw==",
1505
- "signed_at": "2026-05-12T16:58:58.670Z"
1504
+ "signature": "zpEfh181Sc0b0cvRf/31Ir1f8lD4V5tehTogO3TJMxdKmXu06IAK7hrhBcLA/jFBv3xDDwrWW3sHzChVhWDeDA==",
1505
+ "signed_at": "2026-05-12T18:18:21.616Z"
1506
1506
  },
1507
1507
  {
1508
1508
  "name": "sector-federal-government",
@@ -1570,8 +1570,8 @@
1570
1570
  "EU Cybersecurity Certification Scheme on Common Criteria (EUCC) operational — first certificates issued 2024; high-assurance level for government use cases ramping",
1571
1571
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1572
1572
  ],
1573
- "signature": "Wjdo5YXEL8XeNZkaEueG1DOUoyalstNPzQkxD/cwP5iMrJWg/Ly+sC0Oluuqm3aU7d63z55PrbGQCJD0XVZqBg==",
1574
- "signed_at": "2026-05-12T16:58:58.671Z"
1573
+ "signature": "7NpQlPu1DkpY9f+Frv/LLBHWUUe/qTM80c+xeYDxOzweXhvJGE/dnDCjglYHTjxT82L9cVxzBezvLEne20UpBg==",
1574
+ "signed_at": "2026-05-12T18:18:21.616Z"
1575
1575
  },
1576
1576
  {
1577
1577
  "name": "sector-energy",
@@ -1635,8 +1635,8 @@
1635
1635
  "MadIoT-class research on consumer-IoT-driven grid frequency manipulation moving from proof-of-concept to attributed campaigns",
1636
1636
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1637
1637
  ],
1638
- "signature": "c/l7dOHe0Zj6Ag3abUaEie6o0f8M4rhY5aPI9/wG4z6FDue9PzCVw8vUGoITFgg89g97lMfy2C3CE2PegQoFCw==",
1639
- "signed_at": "2026-05-12T16:58:58.671Z"
1638
+ "signature": "4rhyHN5HykK7MQUmhvaTeDGj6Qf5swDd5ry8foh4KBvTkRKxTI/XyxconFGm5FASnySGPLMxX6m4JZAq5wiNBg==",
1639
+ "signed_at": "2026-05-12T18:18:21.617Z"
1640
1640
  },
1641
1641
  {
1642
1642
  "name": "api-security",
@@ -1704,8 +1704,8 @@
1704
1704
  ],
1705
1705
  "d3fend_refs": [],
1706
1706
  "last_threat_review": "2026-05-11",
1707
- "signature": "9FgcJvYeo07QxQ+mnVRQk4jYLDMO/AVSXMs8cueO2f/qMOTQmrhBMVhj5ze7hzvXpGkp7EK/3Q1XKqde61JMAg==",
1708
- "signed_at": "2026-05-12T16:58:58.671Z"
1707
+ "signature": "hS1izPhETclITK7fp6R67dhy+wFDti/YsJ2M5I1gDjeWZYK41WuxeYSyt5xEHbCr3WCGDFJe77jkK1MWkxk2BA==",
1708
+ "signed_at": "2026-05-12T18:18:21.617Z"
1709
1709
  },
1710
1710
  {
1711
1711
  "name": "cloud-security",
@@ -1785,8 +1785,8 @@
1785
1785
  "eBPF-based runtime detection coverage of confidential-computing enclaves (AWS Nitro Enclaves, Azure Confidential VMs, GCP Confidential Space) — partial visibility is a tracked detection gap",
1786
1786
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
1787
1787
  ],
1788
- "signature": "xRA0XZf7VPtuBtbsm41bay9yBLphw/hlL3YxIUrpko5g9ldM3oJe9o1qSwzIj/wSnQSI29qqPpNsnlks+HEOCA==",
1789
- "signed_at": "2026-05-12T16:58:58.672Z"
1788
+ "signature": "kuatqNZoRnv+oeyrxbnk+m37JRBIgRAWnDp0/IYLnoBOybiG09RzLILJraxjhvdSNCgo7WXTeBO3Y6a3Ji9MAA==",
1789
+ "signed_at": "2026-05-12T18:18:21.617Z"
1790
1790
  },
1791
1791
  {
1792
1792
  "name": "container-runtime-security",
@@ -1847,8 +1847,8 @@
1847
1847
  ],
1848
1848
  "d3fend_refs": [],
1849
1849
  "last_threat_review": "2026-05-11",
1850
- "signature": "GcU50DStuN1gU/Evm/sFRgeieQbqffVp12rgbGnasRX89Q7kM4ltFXB+bgCXHIvICzYb78hPIifWQb9UVupWBQ==",
1851
- "signed_at": "2026-05-12T16:58:58.672Z"
1850
+ "signature": "Btb3/7fjPFopFVdxP7+E6n322gnAAwd7OPrnuqatq6c1rXTD9aXKxiBeCmWxs8zYbIbE/lFoe9R2g6uTp8ZDBg==",
1851
+ "signed_at": "2026-05-12T18:18:21.618Z"
1852
1852
  },
1853
1853
  {
1854
1854
  "name": "mlops-security",
@@ -1918,8 +1918,8 @@
1918
1918
  "EU AI Act high-risk technical-file implementing acts (2026-2027) — operational requirements for Article 10 / 13 / 15 documentation may pin ML-BOM or model-signing",
1919
1919
  "MITRE ATLAS v5.2 — track AML.T0010 sub-technique expansion and any new MLOps-pipeline-specific TTPs"
1920
1920
  ],
1921
- "signature": "onIazpFoL1t4PMNRsoF06ggnl7BzCKjt0x+ZmVfWfyt1V06DgllsrbN3AAz4+g4jW2Sc71q0vIFKfwEUWpGVAQ==",
1922
- "signed_at": "2026-05-12T16:58:58.672Z"
1921
+ "signature": "TBWnlgdllW7K1F10HCJ7p4dbLeS3lyNWm+7mNNtyZu7jB1V5AauG1P7sb1nLLqwKqeGlHS1F0eh/BNiuAvkABg==",
1922
+ "signed_at": "2026-05-12T18:18:21.618Z"
1923
1923
  },
1924
1924
  {
1925
1925
  "name": "incident-response-playbook",
@@ -1980,8 +1980,8 @@
1980
1980
  "IL INCD Incident Response Process v4 (slated for 2026-2027) consolidating AI-incident sub-class",
1981
1981
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
1982
1982
  ],
1983
- "signature": "P0Yv4CtqbnBNP6nSIxQUYYHL7T7ci+iE7iE2UXVfnMPeWVdKG2nvRePjBXc3JZTLima1Txn/I5ocDNhLTIeUAQ==",
1984
- "signed_at": "2026-05-12T16:58:58.673Z"
1983
+ "signature": "FVAXpD6sIoOLQSPtZSLLsXQnc2o2hRwiFj4xK8zEWJVkUWGqvAWRrngie7O2DRKIbWqjO5h9EevVYSzhwYHCAA==",
1984
+ "signed_at": "2026-05-12T18:18:21.618Z"
1985
1985
  },
1986
1986
  {
1987
1987
  "name": "email-security-anti-phishing",
@@ -2033,8 +2033,8 @@
2033
2033
  "cwe_refs": [],
2034
2034
  "d3fend_refs": [],
2035
2035
  "last_threat_review": "2026-05-11",
2036
- "signature": "2pv81lLRbazpHqundCANb3YiLB4lkVsYctIDvI8rxSvHxhPS9jYXqmAoB5APSdDuOaew6XqpfZOehQUj9WmyBw==",
2037
- "signed_at": "2026-05-12T16:58:58.673Z"
2036
+ "signature": "0HDt3Qklee4FQeKoZfwr+8qdq2pVDS0a+c7JxVw1hV/bl8+YTPaPjPTAhQUnbhUCa5cGo7G4MBQ1AifQTMJdDA==",
2037
+ "signed_at": "2026-05-12T18:18:21.619Z"
2038
2038
  },
2039
2039
  {
2040
2040
  "name": "age-gates-child-safety",
@@ -2101,8 +2101,8 @@
2101
2101
  "France SREN (Securing and Regulating the Digital Space) Act 2024 — ARCOM age-verification referential for adult content services; double-anonymity model under deployment",
2102
2102
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2103
2103
  ],
2104
- "signature": "BJ/YYnGVXeSBaR9oWAVrcNX7Wz+kE8R4CghX6+XEI/qY89fyrkKNNwo2veqqf49wffJhHVJ1wTp8ZDECjNp+Dw==",
2105
- "signed_at": "2026-05-12T16:58:58.673Z"
2104
+ "signature": "UyPSKUztZI/daHCRTnAh6ryoKLX4xyjuG+EaNMPRVuCz2gANGl1F/NozDsw7R2koMUwSFoiYTzwqDvo1tpuKAg==",
2105
+ "signed_at": "2026-05-12T18:18:21.619Z"
2106
2106
  }
2107
2107
  ]
2108
2108
  }
@@ -305,6 +305,19 @@ function runCurrency() {
305
305
  }
306
306
 
307
307
  async function runReport(format) {
308
+ // v0.11.6 (#98): validate format positional. Pre-0.11.6 unknown formats
309
+ // emitted a generic "# exceptd Report" header — silently accepted any
310
+ // string. Now: reject with structured JSON error matching other verbs.
311
+ const VALID_REPORT_FORMATS = ['executive', 'technical', 'compliance', 'csaf'];
312
+ if (!VALID_REPORT_FORMATS.includes(format)) {
313
+ process.stderr.write(JSON.stringify({
314
+ ok: false,
315
+ error: `report: format "${format}" not in accepted set ${JSON.stringify(VALID_REPORT_FORMATS)}.`,
316
+ verb: 'report',
317
+ }) + '\n');
318
+ process.exit(2);
319
+ }
320
+
308
321
  // v0.11.1 feature #55: `report csaf` emits a CSAF 2.0 envelope covering
309
322
  // every scanned finding + dispatched plan + currency posture. Useful for
310
323
  // VEX downstreams that ingest CSAF JSON.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.11.5",
3
+ "version": "0.11.8",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
@@ -84,7 +84,7 @@
84
84
  "validate-package": "node lib/validate-package.js",
85
85
  "refresh-sbom": "node scripts/refresh-sbom.js",
86
86
  "predeploy": "node scripts/predeploy.js",
87
- "prepublishOnly": "node scripts/predeploy.js && node lib/validate-package.js",
87
+ "prepublishOnly": "node -e \"if(process.env.EXCEPTD_SKIP_PREPUBLISH_PREDEPLOY!=='1'){const r=require('child_process').spawnSync(process.execPath,['scripts/predeploy.js'],{stdio:'inherit'});if(r.status){process.exit(r.status)}}\" && node lib/validate-package.js",
88
88
  "test:docker": "docker build --target predeploy -t exceptd-test:predeploy -f docker/test.Dockerfile . && docker run --rm exceptd-test:predeploy",
89
89
  "test:docker:fresh": "docker build --target fresh-bootstrap -t exceptd-test:fresh-bootstrap -f docker/test.Dockerfile . && docker run --rm exceptd-test:fresh-bootstrap",
90
90
  "scan": "node orchestrator/index.js scan",
package/sbom.cdx.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:396b0919-1153-402c-9438-12692eb441ce",
4
+ "serialNumber": "urn:uuid:26c2d3fe-4c6d-465d-8e09-c069cabf686f",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-12T16:58:59.686Z",
7
+ "timestamp": "2026-05-12T18:18:22.476Z",
8
8
  "tools": [
9
9
  {
10
10
  "name": "hand-written",
@@ -13,10 +13,10 @@
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.11.5",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.11.8",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.11.5",
19
+ "version": "0.11.8",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,11 +25,11 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.11.5",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.11.8",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.5"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.8"
33
33
  },
34
34
  {
35
35
  "type": "vcs",