@blamejs/exceptd-skills 0.11.4 → 0.11.7

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,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.7 — 2026-05-12
4
+
5
+ **Republish of v0.11.6 (which failed CI publish). Adds CI publish-gate fix.**
6
+
7
+ ### CI fix
8
+
9
+ 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.
10
+
11
+ 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.
12
+
13
+ ### What's in this release
14
+
15
+ 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:
16
+
17
+ - **#91** CSAF + OpenVEX include framework_gap_mapping (was: empty bundles for posture-only playbooks)
18
+ - **#92** CSAF tracking.current_release_date populated (spec §3.2.1.12)
19
+ - **#93** SARIF rule definitions for every referenced ruleId (spec §3.27.3)
20
+ - **#94** lint missing_required_artifact downgraded error → warn (align with runner)
21
+ - **#95** default human-readable output for `attest list` + `lint` on TTY
22
+ - **#96** `--strict-preconditions` flag escalates warn-level preconditions to exit 1
23
+ - **#97** `doctor --fix` runs before JSON early-return (was no-op in `--json` mode)
24
+ - **#98** `attest export` + `report` validate `--format` against accepted set
25
+
26
+ ### Workflow improvement
27
+
28
+ 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.
29
+
30
+ ## 0.11.6 — 2026-05-12
31
+
32
+ **Patch: items 91-98 + regression coverage extended to 35 cases.**
33
+
34
+ ### Critical
35
+
36
+ - **#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).
37
+
38
+ ### Bugs
39
+
40
+ - **#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).
41
+ - **#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.
42
+ - **#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.
43
+ - **#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.
44
+ - **#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.
45
+ - **#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"`).
46
+ - **#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.
47
+
48
+ ### Test infrastructure
49
+
50
+ 35 cases in `tests/operator-bugs.test.js` (8 new for 91-98). 322 tests pass total. Future bug fixes continue to land here.
51
+
52
+ ## 0.11.5 — 2026-05-12
53
+
54
+ **Patch: items 82-90 + permanent regression suite at `tests/operator-bugs.test.js`.**
55
+
56
+ Every operator-reported bug fixed across the v0.9.5 → v0.11.x arc now lands as a named test case in `tests/operator-bugs.test.js`. Re-introductions surface at `npm test`, not at user re-report. 27 cases on day one covering items #17, #18, #19, #31, #32, #33, #46, #58, #62, #65, #71, #73, #76, #82, #83, #85, #87.
57
+
58
+ ### Critical
59
+
60
+ - **#82 SARIF / CSAF / OpenVEX rendered empty bundles** when the playbook had no catalogued CVEs. crypto-codebase / library-author have `domain.cve_refs: []` by design (they check process / posture, not catalogue CVEs), so the renderers had nothing to populate. Pre-0.11.5 a successful run with 9 indicators firing produced `vulnerabilities: 0` / `results: 0` / `statements: 0`. Now: indicators that fire (verdict: hit) and framework gaps are first-class SARIF results / CSAF vulnerabilities / OpenVEX statements. Each fired indicator becomes a SARIF result with `kind: indicator_hit` + a pseudo-CVE id under the `exceptd:` namespace for CSAF/OpenVEX. SARIF + CSAF + OpenVEX bundles now meaningfully integrate with GitHub Code Scanning / VEX downstreams / supply-chain tooling even for posture-only playbooks.
61
+
62
+ ### Bugs
63
+
64
+ - **#83 lint and run disagreed on shape validity.** Lint walked the raw submission and only matched observations whose key was a known artifact id. The runner's `normalizeSubmission` followed `val.artifact` indirection — so observations with arbitrary keys (`obs-1`, `obs-2`) and an `artifact:` field route correctly. Fix: lint now runs the same `normalizeSubmission` the runner does, then validates the canonical normalized shape. The user's proposed fix — single observations-normalizer module that lint, run, and format renderers all consume — landed.
65
+
66
+ - **#85 `from_observation` always null.** The diagnostic field on `indicators_evaluated[]` is now populated with the observation key that drove each indicator outcome (when supplied via flat-shape observation + indicator + result). Lets operators trace "which observation produced this verdict" without guessing.
67
+
68
+ - **#86 / #76 `--format garbage` was silent.** v0.11.4 fixed it for `run`; this release fixes the same surface on `ci`. Both now emit `{ok:false, error, verb}` JSON to stderr with non-zero exit when an unknown format is requested.
69
+
70
+ - **#90 legacy verbs in help.** v0.10.x legacy verbs (plan / govern / direct / look / scan / dispatch / etc) appeared in the help output alongside their v0.11 replacements. Operators copy-pasting from `exceptd help | grep '^ [a-z]'` ended up using legacy verbs and missed the new ones. Each legacy entry is now prefixed with `[DEPRECATED]` so the grep pattern still excludes them.
71
+
72
+ ### Deferred (confirmed not yet shipped)
73
+
74
+ - **#88 default-output flip incomplete.** `emit()` indents JSON on TTY (improvement over compact JSON); `discover`/`doctor`/`ask`/`refresh` use custom human renderers. `brief`/`run`/`attest list`/`lint` still emit JSON because their data is too rich for a compact human view. Indented-JSON-on-TTY is the v0.11.x answer; per-verb human renderers continue to be incremental.
75
+
76
+ - **#89 warn-level preconditions exit 0.** `on_fail: halt` correctly exits 1; `on_fail: warn` exits 0 with `preflight_issues` populated. The operator wants warn-level to also fail CI gates — `--strict-preconditions` flag deferred to v0.11.6. Today: use `exceptd ci` for CI gates (correctly maps detected/escalate to exit 2).
77
+
78
+ ### Test infrastructure
79
+
80
+ - New: `tests/operator-bugs.test.js` (27 cases, all green). Future bug fixes land here as named cases so the audit script becomes part of CI.
81
+
3
82
  ## 0.11.4 — 2026-05-12
4
83
 
5
84
  **Patch: high-impact #71 fix + items 72-77.**
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
@@ -245,17 +245,26 @@ v0.11.0 canonical surface
245
245
  v0.10.x compatibility (will be removed in v0.12)
246
246
  ────────────────────────────────────────────────
247
247
 
248
- These verbs still work but emit a one-time deprecation banner. Migrate to
249
- the v0.11.0 verb shown:
250
-
251
- plan → brief --all govern → brief <pb> --phase govern
252
- direct → brief <pb> --phase direct look → brief <pb> --phase look
253
- ingestrun reattest attest diff
254
- list-attestationsattest list scan discover --scan-only
255
- dispatchdiscover currency doctor --currency
256
- verifydoctor --signatures validate-cves → doctor --cves
257
- validate-rfcsdoctor --rfcs watchlist → watch
258
- prefetch → refresh --no-network build-indexesrefresh --indexes-only
248
+ These verbs still work but emit a one-time deprecation banner. The
249
+ [DEPRECATED] prefix is included so \`exceptd help | grep '^ [a-z]'\`
250
+ doesn't surface them in the active-verbs list. Migrate to v0.11:
251
+
252
+ [DEPRECATED] plan → brief --all
253
+ [DEPRECATED] govern <pb> brief <pb> --phase govern
254
+ [DEPRECATED] direct <pb> brief <pb> --phase direct
255
+ [DEPRECATED] look <pb> brief <pb> --phase look
256
+ [DEPRECATED] ingest run
257
+ [DEPRECATED] reattest <sid> attest diff <sid>
258
+ [DEPRECATED] list-attestationsattest list
259
+ [DEPRECATED] scan → discover --scan-only
260
+ [DEPRECATED] dispatch → discover
261
+ [DEPRECATED] currency → doctor --currency
262
+ [DEPRECATED] verify → doctor --signatures
263
+ [DEPRECATED] validate-cves → doctor --cves
264
+ [DEPRECATED] validate-rfcs → doctor --rfcs
265
+ [DEPRECATED] watchlist → watch
266
+ [DEPRECATED] prefetch → refresh --no-network
267
+ [DEPRECATED] build-indexes → refresh --indexes-only
259
268
 
260
269
  Output: default human-readable (v0.11.0). --json for machine output.
261
270
  --pretty for indented JSON.
@@ -427,12 +436,18 @@ function parseArgs(argv, opts) {
427
436
  return out;
428
437
  }
429
438
 
430
- function emit(obj, pretty) {
431
- // v0.11.2 bug #60: when stdout is a TTY (interactive use), emit indented
432
- // JSON instead of single-line much more readable. Piped to a file or
433
- // tool? Default to compact one-line JSON. --pretty forces indented
434
- // 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).
435
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
+ }
436
451
  const indent = pretty || (interactive && !pretty);
437
452
  const s = indent ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
438
453
  process.stdout.write(s + "\n");
@@ -479,7 +494,7 @@ function dispatchPlaybook(cmd, argv) {
479
494
  bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives",
480
495
  "ci", "latest", "diff-from-latest", "explain", "signal-list", "ack",
481
496
  "force-overwrite", "no-stream", "block-on-jurisdiction-clock",
482
- "json-stdout-only", "fix", "human", "json"],
497
+ "json-stdout-only", "fix", "human", "json", "strict-preconditions"],
483
498
  multi: ["playbook", "format"],
484
499
  });
485
500
  // v0.11.2 bug #60: flip defaults to human-readable. JSON via explicit --json
@@ -493,6 +508,8 @@ function dispatchPlaybook(cmd, argv) {
493
508
  // ai-run/ci/attest show/export/diff/verify) fall back to indented JSON
494
509
  // labeled as such — better than no signal.
495
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;
496
513
  const pretty = !!args.pretty;
497
514
  const runOpts = {
498
515
  airGap: !!args["air-gap"],
@@ -818,7 +835,6 @@ function cmdLint(runner, args, runOpts, pretty) {
818
835
  catch (e) { return emitError(`lint: failed to read evidence: ${e.message}`, { evidence: evidencePath }, pretty); }
819
836
 
820
837
  const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
821
- const resolved = runner._resolvedPhase;
822
838
  const lookPhase = pb.phases?.look || {};
823
839
  const detectPhase = pb.phases?.detect || {};
824
840
 
@@ -827,31 +843,48 @@ function cmdLint(runner, args, runOpts, pretty) {
827
843
  const knownIndicators = new Set((detectPhase.indicators || []).map(i => i.id));
828
844
  const knownPreconditions = new Set((pb._meta?.preconditions || []).map(p => p.id));
829
845
 
830
- // Support both flat (observations) and nested (artifacts/signal_overrides) shapes.
846
+ // v0.11.5 #83: shared shape contract with runner. Pre-0.11.5 lint
847
+ // walked the raw submission and only matched observations whose key was
848
+ // a known artifact id. The runner's normalizeSubmission follows
849
+ // `val.artifact` indirection — so observations with arbitrary keys
850
+ // (obs-1, obs-2) and an `artifact:` field route correctly. Lint must
851
+ // do the same normalization before validating, or lint and run disagree
852
+ // on what's a valid submission.
853
+ const normalized = runner.normalizeSubmission(submission, pb);
831
854
  const flat = submission.observations || null;
832
- const artifactsKey = flat ? flat : (submission.artifacts || {});
833
- const signalsKey = flat ? flat : (submission.signal_overrides || {});
834
855
 
856
+ // After normalize, validation walks the canonical nested shape.
835
857
  const missingRequired = requiredArtifacts.filter(id => {
836
- const a = artifactsKey[id];
837
- return !a || (flat ? !a.captured : !a.captured);
858
+ const a = normalized.artifacts && normalized.artifacts[id];
859
+ return !a || !a.captured;
838
860
  });
839
861
 
840
- const unknownArtifactKeys = Object.keys(submission.artifacts || {})
862
+ const unknownArtifactKeys = Object.keys(normalized.artifacts || {})
841
863
  .filter(k => !knownArtifacts.has(k));
842
- const unknownSignalKeys = Object.keys(submission.signal_overrides || {})
864
+ const unknownSignalKeys = Object.keys(normalized.signal_overrides || {})
843
865
  .filter(k => !knownIndicators.has(k));
844
866
  const unknownObservationKeys = flat
845
- ? Object.keys(flat).filter(k => !knownArtifacts.has(k) && !knownIndicators.has(k) && !knownPreconditions.has(k))
867
+ ? Object.keys(flat).filter(k => {
868
+ // Skip observations with explicit `artifact:` indirection — those
869
+ // are valid by-design even when the key doesn't match a known artifact.
870
+ const v = flat[k];
871
+ if (v && typeof v === "object" && v.artifact) return false;
872
+ return !knownArtifacts.has(k) && !knownIndicators.has(k) && !knownPreconditions.has(k);
873
+ })
846
874
  : [];
847
875
 
848
876
  const unsuppliedPreconditions = [...knownPreconditions].filter(
849
- p => !((submission.precondition_checks || {}).hasOwnProperty(p) || (flat || {}).hasOwnProperty(p))
877
+ p => !(((submission.precondition_checks || {}).hasOwnProperty(p)) || ((normalized.precondition_checks || {}).hasOwnProperty(p)))
850
878
  );
851
879
 
852
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.
853
886
  for (const id of missingRequired) {
854
- 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'.` });
855
888
  }
856
889
  for (const k of unknownArtifactKeys) {
857
890
  issues.push({ severity: "warn", kind: "unknown_artifact_key", key: k, hint: `Not in playbook ${playbookId} look.artifacts[]. Recognized: ${[...knownArtifacts].slice(0, 10).join(", ")}…` });
@@ -866,33 +899,19 @@ function cmdLint(runner, args, runOpts, pretty) {
866
899
  issues.push({ severity: "warn", kind: "unknown_observation_key", key: k });
867
900
  }
868
901
 
869
- // #71 (v0.11.3): when a submission is flat-shape with all captured artifacts
870
- // but no indicator+result inline and no verdict.classification — detect()
871
- // will return "inconclusive" because nothing drives the indicator decisions.
872
- // Lint must surface this so operators don't ship a half-shape evidence file
873
- // that passes lint but produces an inconclusive run.
902
+ // #71 (v0.11.3) + #83 (v0.11.5): when a submission is flat-shape but the
903
+ // post-normalize signal_overrides is empty AND no verdict.classification
904
+ // is supplied, detect() will return inconclusive. Surface this before run.
874
905
  if (flat) {
875
- const observationsWithoutIndicator = Object.entries(flat).filter(([k, v]) => {
876
- if (!knownArtifacts.has(k)) return false; // unknown keys flagged elsewhere
877
- if (typeof v !== "object" || v === null) return false;
878
- const captured = v.captured !== false;
879
- return captured && !(v.indicator && v.result);
880
- });
881
906
  const verdictClass = submission.verdict?.classification;
882
907
  const verdictWillDrive = verdictClass === "clean" || verdictClass === "not_detected" || verdictClass === "detected" || verdictClass === "inconclusive";
883
- if (observationsWithoutIndicator.length > 0 && !verdictWillDrive && Object.keys(submission.signal_overrides || {}).length === 0) {
884
- for (const [k] of observationsWithoutIndicator) {
885
- issues.push({
886
- severity: "warn",
887
- kind: "observation_lacks_indicator_result",
888
- observation_key: k,
889
- hint: `Artifact "${k}" captured without "indicator" + "result" fields. detect will return 'inconclusive' for this indicator. Either add { "indicator": "<id>", "result": "hit"|"miss"|"inconclusive" } per observation, OR supply verdict.classification at the submission root to drive the overall verdict.`,
890
- });
891
- }
908
+ const normalizedHasOverrides = Object.keys(normalized.signal_overrides || {}).length > 0;
909
+ if (!verdictWillDrive && !normalizedHasOverrides) {
910
+ const observationsCount = Object.keys(flat).length;
892
911
  issues.push({
893
912
  severity: "info",
894
913
  kind: "detect_will_be_inconclusive",
895
- hint: `Flat submission shape with ${observationsWithoutIndicator.length} captured artifact(s) but no indicator+result inline and no verdict.classification. detect() will return 'inconclusive'. Run \`exceptd run ${playbookId} --signal-list\` to see the indicator IDs the playbook recognizes.`,
914
+ hint: `Flat submission with ${observationsCount} observation(s) but no indicator+result fields and no verdict.classification. detect() will return 'inconclusive'. Each observation needs { "indicator": "<id>", "result": "hit"|"miss"|"inconclusive" } to drive an indicator outcome. Run \`exceptd run ${playbookId} --signal-list\` for the indicator IDs.`,
896
915
  });
897
916
  }
898
917
  }
@@ -910,7 +929,20 @@ function cmdLint(runner, args, runOpts, pretty) {
910
929
  info: issues.filter(i => i.severity === "info").length,
911
930
  },
912
931
  issues,
913
- }, 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
+ });
914
946
  if (!ok) process.exitCode = 1;
915
947
  }
916
948
 
@@ -1266,6 +1298,21 @@ function cmdRun(runner, args, runOpts, pretty) {
1266
1298
  process.exit(1);
1267
1299
  }
1268
1300
 
1301
+ // v0.11.6 (#96): --strict-preconditions escalates warn-level preflight
1302
+ // issues to exit 1. Default (without the flag) preserves the existing
1303
+ // behavior where warn-level issues stay informational. CI gates wanting
1304
+ // "fail on any unverified precondition" pass this flag.
1305
+ if (args["strict-preconditions"] && result && Array.isArray(result.preflight_issues)) {
1306
+ const warnIssues = result.preflight_issues.filter(i =>
1307
+ i.kind === "precondition_unverified" || i.kind === "precondition_warn"
1308
+ );
1309
+ if (warnIssues.length > 0) {
1310
+ process.stderr.write(`[exceptd run] --strict-preconditions: ${warnIssues.length} unverified/warn precondition(s) — exit 1.\n`);
1311
+ emit(result, pretty);
1312
+ process.exit(1);
1313
+ }
1314
+ }
1315
+
1269
1316
  // --diff-from-latest: compare evidence_hash against the most recent prior
1270
1317
  // attestation for this playbook. Drift mode for cron baselines.
1271
1318
  // We've already persisted the CURRENT attestation above, so the find must
@@ -1981,6 +2028,13 @@ function cmdAttest(runner, args, runOpts, pretty) {
1981
2028
  let formatRaw = args.format || "json";
1982
2029
  if (Array.isArray(formatRaw)) formatRaw = formatRaw[0];
1983
2030
  const format = formatRaw === "csaf-2.0" ? "csaf" : formatRaw;
2031
+ // v0.11.6 (#98): validate against accepted set. Pre-0.11.6 unknown
2032
+ // formats fell through to the default redacted JSON output, silently
2033
+ // accepting any value the operator passed.
2034
+ const VALID_EXPORT_FORMATS = ["json", "csaf", "csaf-2.0"];
2035
+ if (!VALID_EXPORT_FORMATS.includes(formatRaw)) {
2036
+ return emitError(`attest export: --format "${formatRaw}" not in accepted set ${JSON.stringify(VALID_EXPORT_FORMATS)}.`, null, pretty);
2037
+ }
1984
2038
  const redacted = attestations.map(a => ({
1985
2039
  session_id: a.session_id,
1986
2040
  playbook_id: a.playbook_id,
@@ -2446,6 +2500,31 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2446
2500
  },
2447
2501
  };
2448
2502
 
2503
+ // v0.11.6 (#97): --fix runs BEFORE the JSON early-return so `exceptd doctor
2504
+ // --fix --json` actually fixes (was a no-op pre-0.11.6). Re-runs the
2505
+ // signing check after fix so the returned JSON reflects the post-fix state.
2506
+ if (args.fix && checks.signing && !checks.signing.private_key_present) {
2507
+ process.stderr.write("[doctor --fix] generating Ed25519 keypair via `node lib/sign.js generate-keypair`...\n");
2508
+ const r = require("child_process").spawnSync(process.execPath, [path.join(PKG_ROOT, "lib", "sign.js"), "generate-keypair"], {
2509
+ stdio: ["ignore", "pipe", "pipe"], cwd: PKG_ROOT,
2510
+ });
2511
+ if (r.status === 0) {
2512
+ // Re-verify the private key is now present so the JSON output reflects
2513
+ // the fix.
2514
+ const keyPath = path.join(process.cwd(), ".keys", "private.pem");
2515
+ const fallback = path.join(PKG_ROOT, ".keys", "private.pem");
2516
+ const present = fs.existsSync(keyPath) || fs.existsSync(fallback);
2517
+ checks.signing = { ok: present, severity: present ? "info" : "warn", private_key_present: present, can_sign_attestations: present };
2518
+ out.checks = checks;
2519
+ out.summary.fix_applied = "ed25519_keypair_generated";
2520
+ process.stderr.write("[doctor --fix] keypair generated — re-checking signing status.\n");
2521
+ } else {
2522
+ out.summary.fix_attempted = "ed25519_keypair_generation_failed";
2523
+ out.summary.fix_exit_code = r.status;
2524
+ process.stderr.write(`[doctor --fix] generation failed (exit=${r.status}); run \`node lib/sign.js generate-keypair\` manually.\n`);
2525
+ }
2526
+ }
2527
+
2449
2528
  if (wantJson) {
2450
2529
  emit(out, indent);
2451
2530
  if (!allGreen) process.exitCode = 1;
@@ -2499,17 +2578,14 @@ function cmdDoctor(runner, args, runOpts, pretty) {
2499
2578
  lines.push(`summary: ${errorList.length} fail / ${warnList.length} warn — fail: ${errorList.join(", ")}; warn: ${warnList.join(", ") || "none"}`);
2500
2579
  }
2501
2580
  process.stdout.write(lines.join("\n") + "\n");
2502
- // Bug #69 (v0.11.2): --fix mode for missing private key.
2503
- if (args.fix && checks.signing && !checks.signing.private_key_present) {
2504
- process.stdout.write("\n[doctor --fix] generating Ed25519 keypair via `node lib/sign.js generate-keypair`...\n");
2505
- const r = require("child_process").spawnSync(process.execPath, [path.join(PKG_ROOT, "lib", "sign.js"), "generate-keypair"], { stdio: "inherit", cwd: PKG_ROOT });
2506
- if (r.status === 0) {
2507
- process.stdout.write("[doctor --fix] keypair generated — re-run `exceptd doctor` to confirm.\n");
2508
- } else {
2509
- process.stdout.write(`[doctor --fix] generation failed (exit=${r.status}); run \`node lib/sign.js generate-keypair\` manually.\n`);
2510
- process.exitCode = 1;
2511
- return;
2512
- }
2581
+ // v0.11.6 (#97): --fix already ran above the JSON early-return. Echo the
2582
+ // applied/attempted state here for human readers.
2583
+ if (out.summary.fix_applied) {
2584
+ process.stdout.write(`\n[doctor --fix] ${out.summary.fix_applied} — re-run \`exceptd doctor\` to confirm.\n`);
2585
+ } else if (out.summary.fix_attempted) {
2586
+ 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`);
2587
+ process.exitCode = 1;
2588
+ return;
2513
2589
  }
2514
2590
  if (errorList.length > 0) process.exitCode = 1;
2515
2591
  // Warnings alone do NOT force exit 1 — CI gates use exit 0 to mean "ran
@@ -2557,7 +2633,21 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
2557
2633
  count: entries.length,
2558
2634
  filter: { playbook: args.playbook || null, since: args.since || null },
2559
2635
  roots_searched: [...seenRoots],
2560
- }, pretty);
2636
+ }, pretty, (obj) => {
2637
+ // v0.11.6 (#95) human renderer for attest list: one row per session.
2638
+ const lines = [`attest list — ${obj.count} attestation(s)`];
2639
+ if (obj.count === 0) {
2640
+ lines.push(` (no attestations under ${obj.roots_searched.join(' or ')})`);
2641
+ return lines.join("\n");
2642
+ }
2643
+ lines.push(` ${"session-id".padEnd(20)} ${"playbook".padEnd(16)} ${"captured-at".padEnd(20)} evidence-hash`);
2644
+ lines.push(` ${"-".repeat(20)} ${"-".repeat(16)} ${"-".repeat(20)} ${"-".repeat(20)}`);
2645
+ for (const e of obj.attestations.slice(0, 50)) {
2646
+ lines.push(` ${(e.session_id || "?").padEnd(20)} ${(e.playbook_id || "?").padEnd(16)} ${(e.captured_at || "").slice(0, 19).padEnd(20)} ${e.evidence_hash || ""}`);
2647
+ }
2648
+ if (obj.count > 50) lines.push(` … and ${obj.count - 50} more (use --json for full list)`);
2649
+ return lines.join("\n");
2650
+ });
2561
2651
  }
2562
2652
 
2563
2653
  // ---------------------------------------------------------------------------
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-12T16:00:45.631Z",
3
+ "generated_at": "2026-05-12T17:44:09.922Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "6935c71303fe299a6a52e1776d571e30808cae14ddfadc48b27d9ac2443c423a",
7
+ "manifest.json": "ca19f2d85b4b143dffe5517494455f0a8a2c97bddd028c66ef5a38623503a160",
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
+ MCowBQYDK2VwAyEAaRfxRnf9Z/887Hux2Senf7NSEaAnhGLwLg9oLL1vhDM=
3
3
  -----END PUBLIC KEY-----
@@ -385,7 +385,16 @@ function detect(playbookId, directiveId, agentSubmission = {}) {
385
385
  // v0.11.4 (#73): downstream consumers iterating `indicators_evaluated`
386
386
  // expect an array, not a count. Restore as array; provide
387
387
  // `indicators_evaluated_count` for callers wanting the integer.
388
- indicators_evaluated: indicatorResults.map(i => ({ signal_id: i.id, outcome: i.verdict, confidence: i.confidence })),
388
+ indicators_evaluated: indicatorResults.map(i => ({
389
+ signal_id: i.id,
390
+ outcome: i.verdict,
391
+ confidence: i.confidence,
392
+ // v0.11.5 #85: surface which observation produced this indicator's
393
+ // outcome (when the agent submitted it via flat-shape observation +
394
+ // indicator + result fields). Null when no observation drove the
395
+ // indicator (engine-computed default).
396
+ from_observation: agentSubmission._signal_origins?.[i.id] || null,
397
+ })),
389
398
  indicators_evaluated_count: indicatorResults.length,
390
399
  classification_override_applied: validOverrides.has(override) ? (override === 'clean' ? 'not_detected' : override) : null,
391
400
  submission_shape_seen: agentSubmission._original_shape || (agentSubmission.artifacts ? 'nested (v0.10.x)' : 'empty')
@@ -514,6 +523,12 @@ function analyze(playbookId, directiveId, detectResult, agentSignals = {}) {
514
523
  },
515
524
  framework_gap_mapping: frameworkGaps,
516
525
  escalations,
526
+ // v0.11.5 (#82): expose detect's per-indicator results + classification
527
+ // here so close()'s bundle builders can iterate indicators that fired
528
+ // and emit them as SARIF results / OpenVEX statements / CSAF notes.
529
+ // Prefixed with underscore to signal "for internal/render use".
530
+ _detect_indicators: detectResult.indicators || [],
531
+ _detect_classification: detectResult.classification,
517
532
  vex: vexFilter ? {
518
533
  filter_applied: true,
519
534
  dropped_cve_count: vexDropped.length,
@@ -757,100 +772,179 @@ function analyzeFindingShape(a) {
757
772
  }
758
773
 
759
774
  function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals) {
760
- // CSAF-2.0 shape minimal valid envelope; production GRC submission would
761
- // need full distribution + product_tree population, deferred to the GRC
762
- // integration layer.
775
+ // CSAF-2.0 shape. v0.11.5 (#82): include vulnerabilities for both matched
776
+ // catalogue CVEs AND fired indicators (treated as advisory pseudo-CVEs
777
+ // under `exceptd:` namespace), so playbooks without catalogue CVEs still
778
+ // emit a non-empty bundle.
763
779
  if (format === 'csaf-2.0') {
780
+ const indicatorHits = (analyze._detect_indicators || []).filter(i => i.verdict === 'hit');
781
+ const cveVulns = analyze.matched_cves.map(c => ({
782
+ cve: c.cve_id,
783
+ scores: [{ products: [], cvss_v3: { base_score: c.cvss_score || 0 } }],
784
+ threats: c.active_exploitation === 'confirmed' ? [{ category: 'exploit_status', details: 'Active exploitation confirmed (CISA KEV).' }] : [],
785
+ remediations: [{ category: 'vendor_fix', details: validate.selected_remediation?.description || 'See selected remediation path.' }]
786
+ }));
787
+ const indicatorVulns = indicatorHits.map(i => ({
788
+ // Pseudo-CVE id for indicator findings (CSAF requires `cve` or `ids`).
789
+ ids: [{ system_name: 'exceptd-indicator', text: `${playbook._meta.id}:${i.id}` }],
790
+ notes: [{ category: 'description', text: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? ' / deterministic' : ''}) in playbook ${playbook._meta.id}.` }],
791
+ remediations: [{ category: 'mitigation', details: validate.selected_remediation?.description || `Consult playbook brief: exceptd brief ${playbook._meta.id}.` }],
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();
764
805
  return {
765
806
  document: {
766
807
  category: 'csaf_security_advisory',
767
808
  csaf_version: '2.0',
768
809
  publisher: { category: 'vendor', name: 'exceptd', namespace: 'https://exceptd.com' },
769
- title: `exceptd finding: ${playbook.domain.name} (${analyze.matched_cves.length} catalogued CVEs)`,
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))`,
770
811
  tracking: {
771
812
  id: `exceptd-${playbook._meta.id}-${Date.now()}`,
772
813
  status: 'final',
773
814
  version: playbook._meta.version,
774
- initial_release_date: new Date().toISOString(),
775
- 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' }]
776
821
  }
777
822
  },
778
- vulnerabilities: analyze.matched_cves.map(c => ({
779
- cve: c.cve_id,
780
- scores: [{ products: [], cvss_v3: { base_score: c.cvss_score || 0 } }],
781
- threats: c.active_exploitation === 'confirmed' ? [{ category: 'exploit_status', details: 'Active exploitation confirmed (CISA KEV).' }] : [],
782
- remediations: [{ category: 'vendor_fix', details: validate.selected_remediation?.description || 'See selected remediation path.' }]
783
- })),
823
+ vulnerabilities: [...cveVulns, ...indicatorVulns, ...gapVulns],
784
824
  exceptd_extension: {
825
+ classification: analyze._detect_classification,
785
826
  rwep: analyze.rwep,
786
827
  blast_radius_score: analyze.blast_radius_score,
787
828
  compliance_theater: analyze.compliance_theater_check,
788
829
  framework_gap_mapping: analyze.framework_gap_mapping,
789
830
  evidence_requirements: validate.evidence_requirements,
790
- residual_risk_statement: validate.residual_risk_statement
831
+ residual_risk_statement: validate.residual_risk_statement,
832
+ indicators_fired: indicatorHits.map(i => ({ id: i.id, confidence: i.confidence, deterministic: i.deterministic })),
791
833
  }
792
834
  };
793
835
  }
794
836
 
795
837
  // SARIF 2.1.0 — GitHub Code Scanning / VS Code SARIF Viewer / Azure DevOps
796
- // and most static analysis tooling. One run per playbook directive, one
797
- // result per matched CVE. Each result references a rule (cve_id) and ties
798
- // back to the directive as the "tool" producer.
838
+ // / most static-analysis tooling.
839
+ //
840
+ // v0.11.5 (#82): emit results from BOTH matched_cves AND fired indicators.
841
+ // Pre-0.11.5 we emitted only matched_cves, which produced an empty bundle
842
+ // for playbooks like crypto-codebase / library-author whose domain.cve_refs
843
+ // is intentionally empty (the playbook checks process/posture, not catalog
844
+ // CVEs). Indicators that fire (verdict: hit) and framework gaps are now
845
+ // first-class SARIF results — a clean run still emits a usable bundle.
799
846
  if (format === 'sarif' || format === 'sarif-2.1.0') {
847
+ const cveResults = analyze.matched_cves.map(c => ({
848
+ ruleId: c.cve_id,
849
+ level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note',
850
+ message: { text: `${c.cve_id}: RWEP ${c.rwep}, blast_radius ${analyze.blast_radius_score}. ${validate.selected_remediation?.description || ''}` },
851
+ properties: {
852
+ kind: 'cve_match',
853
+ rwep: c.rwep, cisa_kev: c.cisa_kev, cisa_kev_due_date: c.cisa_kev_due_date,
854
+ active_exploitation: c.active_exploitation, ai_discovered: c.ai_discovered,
855
+ blast_radius_score: analyze.blast_radius_score,
856
+ }
857
+ }));
858
+ const indicatorHits = (analyze._detect_indicators || []).filter(i => i.verdict === 'hit');
859
+ const indicatorResults = indicatorHits.map(i => ({
860
+ ruleId: i.id,
861
+ level: i.deterministic ? 'error' : (i.confidence === 'high' ? 'warning' : 'note'),
862
+ message: { text: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? ' / deterministic' : ''}). Playbook: ${playbook._meta.id}.` },
863
+ properties: { kind: 'indicator_hit', confidence: i.confidence, deterministic: i.deterministic, atlas_ref: i.atlas_ref, attack_ref: i.attack_ref },
864
+ }));
865
+ const gapResults = (analyze.framework_gap_mapping || []).map((g, idx) => ({
866
+ ruleId: `framework-gap-${idx}`,
867
+ level: 'note',
868
+ message: { text: `${g.framework}: ${g.claimed_control} — ${g.actual_gap}${g.required_control ? '. Required: ' + g.required_control : ''}` },
869
+ properties: { kind: 'framework_gap', framework: g.framework, control: g.claimed_control },
870
+ }));
871
+ const cveRules = analyze.matched_cves.map(c => ({
872
+ id: c.cve_id, shortDescription: { text: c.cve_id },
873
+ fullDescription: { text: `RWEP ${c.rwep} · KEV=${c.cisa_kev} · active_exploitation=${c.active_exploitation}` },
874
+ defaultConfiguration: { level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note' },
875
+ helpUri: `https://nvd.nist.gov/vuln/detail/${c.cve_id}`,
876
+ }));
877
+ const indicatorRules = indicatorHits.map(i => ({
878
+ id: i.id, shortDescription: { text: i.id },
879
+ fullDescription: { text: `Indicator from playbook ${playbook._meta.id}. Type: ${i.type}. Confidence: ${i.confidence}.` },
880
+ defaultConfiguration: { level: i.deterministic ? 'error' : (i.confidence === 'high' ? 'warning' : 'note') },
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
+ }));
800
894
  return {
801
895
  $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
802
896
  version: '2.1.0',
803
897
  runs: [{
804
- tool: {
805
- driver: {
806
- name: 'exceptd',
807
- version: playbook._meta.version,
808
- informationUri: 'https://exceptd.com',
809
- rules: analyze.matched_cves.map(c => ({
810
- id: c.cve_id,
811
- shortDescription: { text: c.cve_id },
812
- fullDescription: { text: `RWEP ${c.rwep} · KEV=${c.cisa_kev} · active_exploitation=${c.active_exploitation} · PoC=${c.poc_available}` },
813
- defaultConfiguration: { level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note' },
814
- helpUri: `https://nvd.nist.gov/vuln/detail/${c.cve_id}`,
815
- }))
816
- }
817
- },
818
- results: analyze.matched_cves.map(c => ({
819
- ruleId: c.cve_id,
820
- level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note',
821
- message: { text: `${c.cve_id}: RWEP ${c.rwep}, blast_radius ${analyze.blast_radius_score}. ${validate.selected_remediation?.description || ''}` },
822
- properties: {
823
- rwep: c.rwep,
824
- cisa_kev: c.cisa_kev,
825
- cisa_kev_due_date: c.cisa_kev_due_date,
826
- active_exploitation: c.active_exploitation,
827
- ai_discovered: c.ai_discovered,
828
- blast_radius_score: analyze.blast_radius_score,
829
- framework_gaps: analyze.framework_gap_mapping?.length || 0,
830
- }
831
- }))
898
+ tool: { driver: {
899
+ name: 'exceptd', version: playbook._meta.version, informationUri: 'https://exceptd.com',
900
+ rules: [...cveRules, ...indicatorRules, ...gapRules],
901
+ } },
902
+ results: [...cveResults, ...indicatorResults, ...gapResults],
903
+ invocations: [{ executionSuccessful: true, properties: {
904
+ playbook: playbook._meta.id, classification: analyze._detect_classification || 'unknown',
905
+ rwep_adjusted: analyze.rwep?.adjusted || 0,
906
+ remediation: validate.selected_remediation?.id || null,
907
+ } }],
832
908
  }]
833
909
  };
834
910
  }
835
911
 
836
- // OpenVEX 0.2.0 — supply-chain VEX statements. Each matched CVE becomes a
837
- // statement with status derived from confidence + RWEP. Downstream tools
838
- // (sigstore, in-toto, GUAC) consume this directly.
912
+ // OpenVEX 0.2.0 — supply-chain VEX statements. v0.11.5 (#82): also include
913
+ // statements derived from fired indicators (treated as advisory findings)
914
+ // so playbooks with empty cve_refs still emit a meaningful bundle.
839
915
  if (format === 'openvex' || format === 'openvex-0.2.0') {
840
916
  const issued = new Date().toISOString();
917
+ const cveStatements = analyze.matched_cves.map(c => ({
918
+ vulnerability: { '@id': c.cve_id, name: c.cve_id },
919
+ status: c.active_exploitation === 'confirmed' ? 'under_investigation' : (c.live_patch_available ? 'fixed' : 'affected'),
920
+ timestamp: issued,
921
+ action_statement: validate.selected_remediation?.description || null,
922
+ impact_statement: `RWEP ${c.rwep}. Blast radius ${analyze.blast_radius_score}/5.`
923
+ }));
924
+ const indicatorStatements = (analyze._detect_indicators || []).filter(i => i.verdict === 'hit').map(i => ({
925
+ vulnerability: { '@id': `exceptd:${playbook._meta.id}:${i.id}`, name: i.id },
926
+ status: 'under_investigation',
927
+ timestamp: issued,
928
+ action_statement: validate.selected_remediation?.description || `Run \`exceptd brief ${playbook._meta.id}\` for context.`,
929
+ impact_statement: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? '/deterministic' : ''}) in playbook ${playbook._meta.id}.`,
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
+ }));
841
941
  return {
842
942
  '@context': 'https://openvex.dev/ns/v0.2.0',
843
943
  '@id': `https://exceptd.com/vex/${playbook._meta.id}/${Date.now()}`,
844
944
  author: 'exceptd',
845
945
  timestamp: issued,
846
946
  version: 1,
847
- statements: analyze.matched_cves.map(c => ({
848
- vulnerability: { '@id': c.cve_id, name: c.cve_id },
849
- status: c.active_exploitation === 'confirmed' ? 'under_investigation' : (c.live_patch_available ? 'fixed' : 'affected'),
850
- timestamp: issued,
851
- action_statement: validate.selected_remediation?.description || null,
852
- impact_statement: `RWEP ${c.rwep}. Blast radius ${analyze.blast_radius_score}/5.`
853
- }))
947
+ statements: [...cveStatements, ...indicatorStatements, ...gapStatements],
854
948
  };
855
949
  }
856
950
 
@@ -947,6 +1041,10 @@ function normalizeSubmission(submission, playbook) {
947
1041
  return v; // leave unrecognized values for detect() to decide
948
1042
  };
949
1043
 
1044
+ // v0.11.5 (#85): track which observation produced each signal_override so
1045
+ // detect can emit `from_observation` on each indicator result. Diagnostic
1046
+ // value for operators chasing "which observation drove this verdict".
1047
+ out._signal_origins = out._signal_origins || {};
950
1048
  for (const [key, val] of Object.entries(submission.observations || {})) {
951
1049
  if (knownPreconditions.has(key)) {
952
1050
  out.precondition_checks[key] = val === "ok" || val === true || val === "true";
@@ -957,6 +1055,7 @@ function normalizeSubmission(submission, playbook) {
957
1055
  out.artifacts[aid] = { value: val.value, captured: val.captured !== false };
958
1056
  if (val.indicator && val.result !== undefined) {
959
1057
  out.signal_overrides[val.indicator] = canonicalizeOutcome(val.result);
1058
+ out._signal_origins[val.indicator] = key;
960
1059
  }
961
1060
  }
962
1061
  }
@@ -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-12T15:59:29.819Z",
3
+ "_generated_at": "2026-05-12T17:43:06.691Z",
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.4",
3
+ "version": "0.11.7",
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-12T15:59:29.388Z",
54
+ "signature": "Xk593pj7my6wPJbQBE47khpIUrPsp6N1lW7cE2T/VPPF5T+8C1yGKc9B8VphD7Q08yWFcbwF6HoWpA/+4uG9DA==",
55
+ "signed_at": "2026-05-12T17:43:06.252Z",
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-12T15:59:29.390Z",
118
+ "signature": "nOgUu+LK9fy6ASTCoRGtx3ttgjZCl7WIkKu2wu06JEKVSpL2cKU3ex2tmVAvv11LBmpTH+b/0zvqXlzcxzHnCw==",
119
+ "signed_at": "2026-05-12T17:43:06.255Z",
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-12T15:59:29.390Z",
181
+ "signature": "7FH1J9PlOyvcRCzRmggmenX9fIR0pi/veXihb3TeStcq1Rpuz1KHdOcJLqA9su4t2goYukKKCXHV6hx8hzplAA==",
182
+ "signed_at": "2026-05-12T17:43:06.255Z",
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-12T15:59:29.391Z"
227
+ "signature": "FqTRjHfEgw56pyHnyWzNtnhzDMEePBtmuamtW/iyX+h4yqbvP4Fyr7NRjRs3EgqT4j7oHuEZhV9Jt6ZTBgN4AA==",
228
+ "signed_at": "2026-05-12T17:43:06.255Z"
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-12T15:59:29.392Z"
258
+ "signature": "3fN4yotiIIq76PVTHwozCu28TzDZvWule6vX8SXUT3XXbIBSuvAO0M/euvc3pw3TdZ2UNf78dI18lOCNdJ0aAg==",
259
+ "signed_at": "2026-05-12T17:43:06.256Z"
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-12T15:59:29.392Z"
287
+ "signature": "yZfpk4lQMRXegj2ADWjMmZTchUN6Lxpv587O/0JMzbNkXQtD6FrSAQOBWjx8S7uQ/sTntxgGN7aQQDLxL9RWAA==",
288
+ "signed_at": "2026-05-12T17:43:06.257Z"
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-12T15:59:29.392Z",
324
+ "signature": "ABHkoqee67KdUyDZ3bvF+/DNxjGhPR/ehT6pfOnmUIMmkcQFHpZ0OUVXKiFUANaLgKLP1vg0VEmHOoxpNA3vAA==",
325
+ "signed_at": "2026-05-12T17:43:06.257Z",
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-12T15:59:29.392Z",
381
+ "signature": "+Nd/2tgBnW+mEGX84QvkgR2To2J7kA+lB63BsADDKeCXeebFv6Vo9H1P4vyUkKHfe4fP0ndpy3agIZcUO/e/Dg==",
382
+ "signed_at": "2026-05-12T17:43:06.257Z",
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-12T15:59:29.393Z",
416
+ "signature": "VMNGFvowXLbBjZp5nvWloKkqyqHKhnSzbVRU3gX9quOZJHH56w2M4id+oDsXIjR0CfRRb7eXl/so0Hq4xLBuBQ==",
417
+ "signed_at": "2026-05-12T17:43:06.257Z",
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-12T15:59:29.393Z"
444
+ "signature": "5MaJs7gPCuFlK4oAttLulAPOA1noeV+xD/UqVWaVyRedXZgebBGKjnlE2t1qmTugvxlNIfeAnBZapk+Wz3VAAg==",
445
+ "signed_at": "2026-05-12T17:43:06.258Z"
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-12T15:59:29.394Z"
476
+ "signature": "S/YXUpI/mcG2FpdUTgMsccWBtTaR5A4Ph4QFQw31S9w9Hn/z3sOFHLkb1B5YSwlg+mMOtSIxMdet1eLGSZkTDg==",
477
+ "signed_at": "2026-05-12T17:43:06.258Z"
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-12T15:59:29.394Z"
503
+ "signature": "AKS+JsmhhBtytY2eIMuydjkZOYprWCmQ+RqxyxcVG9XcEI29ZSM/JbVIINQHozFl7OPPrOu1ouiTnk7LOJ86Bg==",
504
+ "signed_at": "2026-05-12T17:43:06.258Z"
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-12T15:59:29.394Z",
555
+ "signature": "oEkK5bLS/G5RIHnxlNFJYdzhTJbKZnkJv+W4iS9UJ/uszZHgZGoxygELPc4kn3FowV5eE988SQYG4WKlXtNzCg==",
556
+ "signed_at": "2026-05-12T17:43:06.259Z",
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-12T15:59:29.395Z"
602
+ "signature": "nPV6YTo1rsNH49qUnZpfoNLEQZXuLNyV05QMUOgXKHYeVDjotYpWhLgyVXlRhjV/fStiA2sWQ0MOnEJ4FBIfDg==",
603
+ "signed_at": "2026-05-12T17:43:06.259Z"
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-12T15:59:29.395Z",
639
+ "signature": "7rirSEONz6O9Yyf46eTyuwkGizCj9FRcNHe5p7Qz6nhJoZQRW5FwW7n9opL0WlbIw8FDBYn1f22zgNUV87L5AQ==",
640
+ "signed_at": "2026-05-12T17:43:06.260Z",
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-12T15:59:29.395Z"
674
+ "signature": "+evehnd2wSBb8uMTlTr5/aTN4bfLjsKzZJk/+OMLMOJrjCt+OuMU7EQC6xMUGeSc4cPEGajghDvq3xVaacV2Dw==",
675
+ "signed_at": "2026-05-12T17:43:06.260Z"
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-12T15:59:29.396Z"
745
+ "signature": "KHOXxloAYf7xqXjm2BaL3HVAZOmb7rMiMh20H/oaIkjN0WD1CnKCrRGPJn867uSFhCh/timkXolaiqD1L/h8Dg==",
746
+ "signed_at": "2026-05-12T17:43:06.260Z"
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-12T15:59:29.396Z"
805
+ "signature": "+ELdD+1AY5DymBitH7wU65CS60NY1nDoLowJAFn7cE5Gr/5jy9BTkyxsm7PEXaSlXWMOkTf/HQ+uyzyxUVD/Bw==",
806
+ "signed_at": "2026-05-12T17:43:06.261Z"
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-12T15:59:29.396Z"
880
+ "signature": "8tFAhXAS8zZN3SUOdn+ZIu7lQ48JMOyBQ8SaObR3L/fDyFmDhufqleY2VzI3yigqlT/D4Y8FYxZHKmzXiALjDw==",
881
+ "signed_at": "2026-05-12T17:43:06.261Z"
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-12T15:59:29.396Z"
957
+ "signature": "8xlk5ZfTKVYqTE2+ifkjTBu/RPqs4MIvX7SpOHl22YDHi7nzJ1ywPhSNYJzoPdPV4AUuWG518EldQJsEIuyuAA==",
958
+ "signed_at": "2026-05-12T17:43:06.261Z"
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-12T15:59:29.397Z"
1014
+ "signature": "AMdLkDx/e3ESI4NAnJhhcaas+Ru8VjrSn6v6RBbmmzoLCGo/vFxGraa1p/qF9udhVG+DdkbwHfbfKK5Im19KDw==",
1015
+ "signed_at": "2026-05-12T17:43:06.262Z"
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-12T15:59:29.397Z"
1081
+ "signature": "pSMHKkyWoZvRIuVtN7Vue51sP5MIy9lSaQa2YSAMhxjptx81cUnPt3S11/Tb9Ea1/eluMNQ+5F25eF2njr4mBQ==",
1082
+ "signed_at": "2026-05-12T17:43:06.262Z"
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-12T15:59:29.397Z"
1137
+ "signature": "qjky+ZTX1DP7uRRMQZq7S7P9/uaJEoB1dy4RZ1l37Q4OO3k2ryfL+7o0Cgm/piuafJfH+dqUeNCRrVefj4r8Dw==",
1138
+ "signed_at": "2026-05-12T17:43:06.262Z"
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-12T15:59:29.398Z"
1189
+ "signature": "F86Zl/I+dBzHYRUuGWsjDQI2F/I/vhzwZUFMqhNfKUzRbMf6mafOX2APCPYTp3eP1DvvvfL3Yc0hb1R5Q4nOAg==",
1190
+ "signed_at": "2026-05-12T17:43:06.262Z"
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-12T15:59:29.398Z"
1239
+ "signature": "D/4d5NcJScNH58ADXsSrVzTmLSWZpUZTdyhtDkJlC0twSMNczOiDsXgYFitBaZgGdv5nVd00viR45mNrsaZ4BQ==",
1240
+ "signed_at": "2026-05-12T17:43:06.263Z"
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-12T15:59:29.398Z"
1313
+ "signature": "UOXaUtpcFjXyDQ70z2PaGu6K3pABtXp+7YzO6eGVGpN1CxXpPq/xW/CnTng6B7wk9WSsqD0OORBJp4VCjiVfAQ==",
1314
+ "signed_at": "2026-05-12T17:43:06.263Z"
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-12T15:59:29.399Z"
1363
+ "signature": "IVKygsrFjiM64fQVbd2PT6jDjs6fm5nKwJSqGfK53gG0S9wdHC4QYuh+LWlI/2ftvIKjjedLQ6FRyTrqpDEuDw==",
1364
+ "signed_at": "2026-05-12T17:43:06.263Z"
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-12T15:59:29.399Z"
1423
+ "signature": "P+CdSu8ZJCNUU4nTa09Voh2PcYF3y/AFJn4v7cjVIGo9FbbqO7MwvGN7cJ+aSRs2/3NMUXX4eupcODslxYyJDw==",
1424
+ "signed_at": "2026-05-12T17:43:06.264Z"
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-12T15:59:29.399Z"
1504
+ "signature": "zpEfh181Sc0b0cvRf/31Ir1f8lD4V5tehTogO3TJMxdKmXu06IAK7hrhBcLA/jFBv3xDDwrWW3sHzChVhWDeDA==",
1505
+ "signed_at": "2026-05-12T17:43:06.264Z"
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-12T15:59:29.400Z"
1573
+ "signature": "7NpQlPu1DkpY9f+Frv/LLBHWUUe/qTM80c+xeYDxOzweXhvJGE/dnDCjglYHTjxT82L9cVxzBezvLEne20UpBg==",
1574
+ "signed_at": "2026-05-12T17:43:06.264Z"
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-12T15:59:29.400Z"
1638
+ "signature": "4rhyHN5HykK7MQUmhvaTeDGj6Qf5swDd5ry8foh4KBvTkRKxTI/XyxconFGm5FASnySGPLMxX6m4JZAq5wiNBg==",
1639
+ "signed_at": "2026-05-12T17:43:06.265Z"
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-12T15:59:29.400Z"
1707
+ "signature": "hS1izPhETclITK7fp6R67dhy+wFDti/YsJ2M5I1gDjeWZYK41WuxeYSyt5xEHbCr3WCGDFJe77jkK1MWkxk2BA==",
1708
+ "signed_at": "2026-05-12T17:43:06.265Z"
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-12T15:59:29.401Z"
1788
+ "signature": "kuatqNZoRnv+oeyrxbnk+m37JRBIgRAWnDp0/IYLnoBOybiG09RzLILJraxjhvdSNCgo7WXTeBO3Y6a3Ji9MAA==",
1789
+ "signed_at": "2026-05-12T17:43:06.265Z"
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-12T15:59:29.401Z"
1850
+ "signature": "Btb3/7fjPFopFVdxP7+E6n322gnAAwd7OPrnuqatq6c1rXTD9aXKxiBeCmWxs8zYbIbE/lFoe9R2g6uTp8ZDBg==",
1851
+ "signed_at": "2026-05-12T17:43:06.266Z"
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-12T15:59:29.401Z"
1921
+ "signature": "TBWnlgdllW7K1F10HCJ7p4dbLeS3lyNWm+7mNNtyZu7jB1V5AauG1P7sb1nLLqwKqeGlHS1F0eh/BNiuAvkABg==",
1922
+ "signed_at": "2026-05-12T17:43:06.266Z"
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-12T15:59:29.402Z"
1983
+ "signature": "FVAXpD6sIoOLQSPtZSLLsXQnc2o2hRwiFj4xK8zEWJVkUWGqvAWRrngie7O2DRKIbWqjO5h9EevVYSzhwYHCAA==",
1984
+ "signed_at": "2026-05-12T17:43:06.267Z"
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-12T15:59:29.402Z"
2036
+ "signature": "0HDt3Qklee4FQeKoZfwr+8qdq2pVDS0a+c7JxVw1hV/bl8+YTPaPjPTAhQUnbhUCa5cGo7G4MBQ1AifQTMJdDA==",
2037
+ "signed_at": "2026-05-12T17:43:06.267Z"
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-12T15:59:29.402Z"
2104
+ "signature": "UyPSKUztZI/daHCRTnAh6ryoKLX4xyjuG+EaNMPRVuCz2gANGl1F/NozDsw7R2koMUwSFoiYTzwqDvo1tpuKAg==",
2105
+ "signed_at": "2026-05-12T17:43:06.267Z"
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.4",
3
+ "version": "0.11.7",
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:057eee67-86e4-4750-80f8-8dfdf5b5c9d3",
4
+ "serialNumber": "urn:uuid:ff8b6540-9c5a-497c-90cb-0b6a012bab82",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-12T15:59:30.308Z",
7
+ "timestamp": "2026-05-12T17:43:07.123Z",
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.4",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.11.7",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.11.4",
19
+ "version": "0.11.7",
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.4",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.11.7",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.4"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.7"
33
33
  },
34
34
  {
35
35
  "type": "vcs",