@blamejs/exceptd-skills 0.11.3 → 0.11.5

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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.5 — 2026-05-12
4
+
5
+ **Patch: items 82-90 + permanent regression suite at `tests/operator-bugs.test.js`.**
6
+
7
+ 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.
8
+
9
+ ### Critical
10
+
11
+ - **#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.
12
+
13
+ ### Bugs
14
+
15
+ - **#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.
16
+
17
+ - **#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.
18
+
19
+ - **#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.
20
+
21
+ - **#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.
22
+
23
+ ### Deferred (confirmed not yet shipped)
24
+
25
+ - **#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.
26
+
27
+ - **#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).
28
+
29
+ ### Test infrastructure
30
+
31
+ - 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.
32
+
33
+ ## 0.11.4 — 2026-05-12
34
+
35
+ **Patch: high-impact #71 fix + items 72-77.**
36
+
37
+ ### Critical fix
38
+
39
+ - **#71 detect didn't accept indicator-result synonyms.** Operators submitting flat-shape evidence with `observation.result: "no_hit"` (the standard vocabulary for years of CI/security tooling) hit the runner's strict `hit|miss|inconclusive` set, falsed every comparison, and ended up with `classification: "inconclusive"` regardless of evidence. This silently broke the new flat-shape submission UX that v0.11.0/v0.11.3 was built around. Same evidence in the legacy `signal_overrides` shape produced the correct `not_detected` verdict.
40
+
41
+ Fix: a `canonicalize()` step in both `normalizeSubmission` and `detect()` maps `no_hit`/`no-hit`/`clean`/`clear`/`not_hit`/`ok`/`pass`/`negative`/`false` → `miss`; `hit`/`detected`/`positive`/`true` → `hit`; `inconclusive`/`unknown`/`unverified`/`null` → `inconclusive`. Operator vocabulary is now normalized to the engine's canonical 3-value set at submission boundary.
42
+
43
+ - **#77 CSAF/OpenVEX bundles auto-fixed.** Downstream of #71: now that detect actually processes signal_overrides correctly, the per-CVE statements in `bundle.vulnerabilities` / `statements` populate when there are matched_cves.
44
+
45
+ ### Bugs
46
+
47
+ - **#72 ci --format silently ignored.** `exceptd ci --scope code --format summary` and the bare command emitted byte-identical full bundles (~350 KB). CI gates couldn't get a compact verdict without piping through jq. Now ci honors `--format summary|markdown|csaf-2.0|sarif|openvex` with the same shortcuts as `run --format`. Summary is a single-line JSON with `session_id + playbooks_run + verdict + counts`.
48
+ - **#73 `indicators_evaluated` type changed silently.** v0.11.3 introduced it as an integer count; downstream consumers iterating `for i in detect.indicators_evaluated` crashed. Restored to an array of `{signal_id, outcome, confidence}`. Added `indicators_evaluated_count` as a peer field for callers wanting the integer.
49
+ - **#76 `ci --format garbage` silent empty stdout.** Invalid format values now return `{ok:false, error, verb:"ci"}` JSON to stderr with exit 2, matching the unified error shape.
50
+
51
+ ### Not addressed in this patch
52
+
53
+ - **#74 default-output flip still incomplete.** `emit()` indents JSON when stdout is a TTY (improvement over compact), but `brief`/`run`/`attest list`/`lint` still emit JSON, not a custom human form. The richer data on `brief`/`run` doesn't have a natural compact human view. Indented-JSON-on-TTY ships as the v0.11.x answer; a true human renderer per verb is deferred. `discover`/`doctor`/`ask`/`refresh` continue with their custom renderers.
54
+ - **#75 preflight-blocked exit 0 for warn-level.** `on_fail: halt` preconditions correctly exit 1; `on_fail: warn` preconditions correctly exit 0 with `preflight_issues` populated. The operator wants warn-level to also fail CI — that's a `--strict-preconditions` flag, deferred to v0.11.5. Today: use `exceptd ci` for CI gates (correctly maps detected/escalate to exit 2); `run` is for single-investigation invocations where warn-level info is appropriate.
55
+
56
+ ### Already shipped (cross-referenced)
57
+
58
+ - #78 `doctor --fix` (v0.11.2).
59
+
3
60
  ## 0.11.3 — 2026-05-12
4
61
 
5
62
  **Patch: operator-reported item #71 + full feature audit findings.**
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.
@@ -818,7 +827,6 @@ function cmdLint(runner, args, runOpts, pretty) {
818
827
  catch (e) { return emitError(`lint: failed to read evidence: ${e.message}`, { evidence: evidencePath }, pretty); }
819
828
 
820
829
  const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
821
- const resolved = runner._resolvedPhase;
822
830
  const lookPhase = pb.phases?.look || {};
823
831
  const detectPhase = pb.phases?.detect || {};
824
832
 
@@ -827,26 +835,38 @@ function cmdLint(runner, args, runOpts, pretty) {
827
835
  const knownIndicators = new Set((detectPhase.indicators || []).map(i => i.id));
828
836
  const knownPreconditions = new Set((pb._meta?.preconditions || []).map(p => p.id));
829
837
 
830
- // Support both flat (observations) and nested (artifacts/signal_overrides) shapes.
838
+ // v0.11.5 #83: shared shape contract with runner. Pre-0.11.5 lint
839
+ // walked the raw submission and only matched observations whose key was
840
+ // a known artifact id. The runner's normalizeSubmission follows
841
+ // `val.artifact` indirection — so observations with arbitrary keys
842
+ // (obs-1, obs-2) and an `artifact:` field route correctly. Lint must
843
+ // do the same normalization before validating, or lint and run disagree
844
+ // on what's a valid submission.
845
+ const normalized = runner.normalizeSubmission(submission, pb);
831
846
  const flat = submission.observations || null;
832
- const artifactsKey = flat ? flat : (submission.artifacts || {});
833
- const signalsKey = flat ? flat : (submission.signal_overrides || {});
834
847
 
848
+ // After normalize, validation walks the canonical nested shape.
835
849
  const missingRequired = requiredArtifacts.filter(id => {
836
- const a = artifactsKey[id];
837
- return !a || (flat ? !a.captured : !a.captured);
850
+ const a = normalized.artifacts && normalized.artifacts[id];
851
+ return !a || !a.captured;
838
852
  });
839
853
 
840
- const unknownArtifactKeys = Object.keys(submission.artifacts || {})
854
+ const unknownArtifactKeys = Object.keys(normalized.artifacts || {})
841
855
  .filter(k => !knownArtifacts.has(k));
842
- const unknownSignalKeys = Object.keys(submission.signal_overrides || {})
856
+ const unknownSignalKeys = Object.keys(normalized.signal_overrides || {})
843
857
  .filter(k => !knownIndicators.has(k));
844
858
  const unknownObservationKeys = flat
845
- ? Object.keys(flat).filter(k => !knownArtifacts.has(k) && !knownIndicators.has(k) && !knownPreconditions.has(k))
859
+ ? Object.keys(flat).filter(k => {
860
+ // Skip observations with explicit `artifact:` indirection — those
861
+ // are valid by-design even when the key doesn't match a known artifact.
862
+ const v = flat[k];
863
+ if (v && typeof v === "object" && v.artifact) return false;
864
+ return !knownArtifacts.has(k) && !knownIndicators.has(k) && !knownPreconditions.has(k);
865
+ })
846
866
  : [];
847
867
 
848
868
  const unsuppliedPreconditions = [...knownPreconditions].filter(
849
- p => !((submission.precondition_checks || {}).hasOwnProperty(p) || (flat || {}).hasOwnProperty(p))
869
+ p => !(((submission.precondition_checks || {}).hasOwnProperty(p)) || ((normalized.precondition_checks || {}).hasOwnProperty(p)))
850
870
  );
851
871
 
852
872
  const issues = [];
@@ -866,33 +886,19 @@ function cmdLint(runner, args, runOpts, pretty) {
866
886
  issues.push({ severity: "warn", kind: "unknown_observation_key", key: k });
867
887
  }
868
888
 
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.
889
+ // #71 (v0.11.3) + #83 (v0.11.5): when a submission is flat-shape but the
890
+ // post-normalize signal_overrides is empty AND no verdict.classification
891
+ // is supplied, detect() will return inconclusive. Surface this before run.
874
892
  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
893
  const verdictClass = submission.verdict?.classification;
882
894
  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
- }
895
+ const normalizedHasOverrides = Object.keys(normalized.signal_overrides || {}).length > 0;
896
+ if (!verdictWillDrive && !normalizedHasOverrides) {
897
+ const observationsCount = Object.keys(flat).length;
892
898
  issues.push({
893
899
  severity: "info",
894
900
  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.`,
901
+ 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
902
  });
897
903
  }
898
904
  }
@@ -3066,25 +3072,53 @@ function cmdCi(runner, args, runOpts, pretty) {
3066
3072
  const rwepValues = results.map(r => r.phases?.analyze?.rwep?.adjusted ?? 0);
3067
3073
  const maxRwepObserved = rwepValues.length ? Math.max(...rwepValues) : 0;
3068
3074
 
3069
- emit({
3070
- verb: "ci",
3071
- session_id: sessionId,
3072
- playbooks_run: ids,
3073
- summary: {
3074
- total: results.length,
3075
- detected: results.filter(r => r.phases?.detect?.classification === "detected").length,
3076
- inconclusive: results.filter(r => r.phases?.detect?.classification === "inconclusive").length,
3077
- not_detected: results.filter(r => ["not_detected", "clean"].includes(r.phases?.detect?.classification)).length,
3078
- blocked: results.filter(r => r && r.ok === false).length,
3079
- max_rwep_observed: maxRwepObserved,
3080
- jurisdiction_clocks_started: results
3081
- .flatMap(r => r.phases?.close?.notification_actions || [])
3082
- .filter(n => n && n.clock_started_at != null).length,
3083
- verdict: fail ? "FAIL" : "PASS",
3084
- fail_reasons: failReasons,
3085
- },
3086
- results,
3087
- }, pretty);
3075
+ const summary = {
3076
+ total: results.length,
3077
+ detected: results.filter(r => r.phases?.detect?.classification === "detected").length,
3078
+ inconclusive: results.filter(r => r.phases?.detect?.classification === "inconclusive").length,
3079
+ not_detected: results.filter(r => ["not_detected", "clean"].includes(r.phases?.detect?.classification)).length,
3080
+ blocked: results.filter(r => r && r.ok === false).length,
3081
+ max_rwep_observed: maxRwepObserved,
3082
+ jurisdiction_clocks_started: results
3083
+ .flatMap(r => r.phases?.close?.notification_actions || [])
3084
+ .filter(n => n && n.clock_started_at != null).length,
3085
+ verdict: fail ? "FAIL" : "PASS",
3086
+ fail_reasons: failReasons,
3087
+ };
3088
+
3089
+ // v0.11.4 (#72): ci --format <fmt> previously emitted the full bundle
3090
+ // regardless of flag. Now honors the same shortcuts as `run --format`:
3091
+ // summary → one-line JSON of session + verdict + counts
3092
+ // markdown → operator-readable digest
3093
+ // csaf → CSAF 2.0 envelope wrapping every result
3094
+ // sarif → SARIF 2.1.0 with results from every playbook
3095
+ // openvex → OpenVEX statements derived from every playbook's matched_cves
3096
+ let formatRaw = args.format;
3097
+ if (Array.isArray(formatRaw)) formatRaw = formatRaw[0];
3098
+ const fmt = formatRaw === "csaf-2.0" ? "csaf" : formatRaw;
3099
+ if (fmt === "summary") {
3100
+ emit({ verb: "ci", session_id: sessionId, playbooks_run: ids, summary }, pretty);
3101
+ } else if (fmt === "markdown") {
3102
+ const lines = [`# exceptd ci summary`, `session-id: ${sessionId}`, `verdict: **${summary.verdict}**`, ``];
3103
+ lines.push(`**Playbooks run:** ${summary.total} (${summary.detected} detected, ${summary.inconclusive} inconclusive, ${summary.not_detected} clean, ${summary.blocked} blocked)`);
3104
+ lines.push(`**Max RWEP observed:** ${summary.max_rwep_observed}`);
3105
+ lines.push(`**Jurisdiction clocks started:** ${summary.jurisdiction_clocks_started}`);
3106
+ if (summary.fail_reasons.length) {
3107
+ lines.push(``, `## Fail reasons`);
3108
+ for (const r of summary.fail_reasons) lines.push(`- ${r}`);
3109
+ }
3110
+ process.stdout.write(lines.join("\n") + "\n");
3111
+ } else if (fmt === "csaf" || fmt === "sarif" || fmt === "openvex") {
3112
+ // Aggregate the per-run bundles_by_format if present.
3113
+ const bundles = results.map(r => r.phases?.close?.evidence_package?.bundles_by_format?.[fmt === "csaf" ? "csaf-2.0" : fmt]).filter(Boolean);
3114
+ emit({ verb: "ci", session_id: sessionId, format: fmt, bundles_count: bundles.length, bundles }, pretty);
3115
+ } else if (fmt && fmt !== "json") {
3116
+ // v0.11.4 (#76): garbage format rejected with structured error, not silent empty stdout.
3117
+ process.stderr.write(JSON.stringify({ ok: false, error: `ci: --format "${fmt}" not in accepted set ["summary","markdown","csaf-2.0","sarif","openvex","json"].`, verb: "ci" }) + "\n");
3118
+ process.exit(2);
3119
+ } else {
3120
+ emit({ verb: "ci", session_id: sessionId, playbooks_run: ids, summary, results }, pretty);
3121
+ }
3088
3122
  if (fail) {
3089
3123
  process.stderr.write(`[exceptd ci] FAIL: ${failReasons.join("; ")}\n`);
3090
3124
  process.exit(2);
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-12T15:46:10.198Z",
3
+ "generated_at": "2026-05-12T17:00:14.346Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "8097ba32340aba872d73dd047bebb5f8d561cda8c55bcd224c67475c84cfac4b",
7
+ "manifest.json": "da39bcf4cd05d571b5d76a009ede9ee721aa783c07764636e0f0a0bd4634a99a",
8
8
  "data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
9
9
  "data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
10
10
  "data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
@@ -304,8 +304,23 @@ function detect(playbookId, directiveId, agentSubmission = {}) {
304
304
  const artifacts = agentSubmission.artifacts || {};
305
305
  const overrides = agentSubmission.signal_overrides || {};
306
306
 
307
+ // v0.11.4 (#71): canonicalize the indicator result vocabulary. Operators
308
+ // submit shapes like "no_hit" / "clean" / "ok" / false from years of
309
+ // CI/security tooling convention; the engine internally uses
310
+ // hit | miss | inconclusive. Without canonicalization every flat-shape
311
+ // observation with result:"no_hit" silently fell through to inconclusive
312
+ // and broke per-indicator detection. Canonicalization happens here so
313
+ // both detect() and normalizeSubmission consumers see the same outcomes.
314
+ const canonicalize = (v) => {
315
+ if (v === true || v === 'hit' || v === 'detected' || v === 'positive') return 'hit';
316
+ if (v === false || v === 'miss' || v === 'no_hit' || v === 'no-hit' || v === 'clean' || v === 'clear' || v === 'not_hit' || v === 'ok' || v === 'pass' || v === 'negative') return 'miss';
317
+ if (v === 'inconclusive' || v === 'unknown' || v === 'unverified' || v === null) return 'inconclusive';
318
+ return null; // truly unknown — fall through
319
+ };
320
+
307
321
  const indicatorResults = (det.indicators || []).map(ind => {
308
- const override = overrides[ind.id];
322
+ const rawOverride = overrides[ind.id];
323
+ const override = canonicalize(rawOverride);
309
324
  let verdict;
310
325
  if (override === 'hit' || override === 'miss' || override === 'inconclusive') {
311
326
  verdict = override;
@@ -365,11 +380,22 @@ function detect(playbookId, directiveId, agentSubmission = {}) {
365
380
  // v0.11.3 #71: surface what detect actually consumed. Operators reading
366
381
  // the detect output now see whether their flat-shape observations + the
367
382
  // signal_overrides + the classification override all reached the runner.
368
- // Pre-0.11.3 detect's output was opaque — "inconclusive" with no
369
- // explanation gave operators no signal about what went wrong.
370
383
  observations_received: Object.keys(agentSubmission.artifacts || {}),
371
384
  signals_received: Object.keys(agentSubmission.signal_overrides || {}),
372
- indicators_evaluated: indicatorResults.length,
385
+ // v0.11.4 (#73): downstream consumers iterating `indicators_evaluated`
386
+ // expect an array, not a count. Restore as array; provide
387
+ // `indicators_evaluated_count` for callers wanting the integer.
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
+ })),
398
+ indicators_evaluated_count: indicatorResults.length,
373
399
  classification_override_applied: validOverrides.has(override) ? (override === 'clean' ? 'not_detected' : override) : null,
374
400
  submission_shape_seen: agentSubmission._original_shape || (agentSubmission.artifacts ? 'nested (v0.10.x)' : 'empty')
375
401
  };
@@ -497,6 +523,12 @@ function analyze(playbookId, directiveId, detectResult, agentSignals = {}) {
497
523
  },
498
524
  framework_gap_mapping: frameworkGaps,
499
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,
500
532
  vex: vexFilter ? {
501
533
  filter_applied: true,
502
534
  dropped_cve_count: vexDropped.length,
@@ -740,16 +772,30 @@ function analyzeFindingShape(a) {
740
772
  }
741
773
 
742
774
  function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals) {
743
- // CSAF-2.0 shape minimal valid envelope; production GRC submission would
744
- // need full distribution + product_tree population, deferred to the GRC
745
- // 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.
746
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
+ }));
747
793
  return {
748
794
  document: {
749
795
  category: 'csaf_security_advisory',
750
796
  csaf_version: '2.0',
751
797
  publisher: { category: 'vendor', name: 'exceptd', namespace: 'https://exceptd.com' },
752
- title: `exceptd finding: ${playbook.domain.name} (${analyze.matched_cves.length} catalogued CVEs)`,
798
+ title: `exceptd finding: ${playbook.domain.name} (${analyze.matched_cves.length} CVE(s), ${indicatorHits.length} indicator hit(s))`,
753
799
  tracking: {
754
800
  id: `exceptd-${playbook._meta.id}-${Date.now()}`,
755
801
  status: 'final',
@@ -758,82 +804,109 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
758
804
  revision_history: [{ number: '1', date: new Date().toISOString(), summary: 'Initial finding emission' }]
759
805
  }
760
806
  },
761
- vulnerabilities: analyze.matched_cves.map(c => ({
762
- cve: c.cve_id,
763
- scores: [{ products: [], cvss_v3: { base_score: c.cvss_score || 0 } }],
764
- threats: c.active_exploitation === 'confirmed' ? [{ category: 'exploit_status', details: 'Active exploitation confirmed (CISA KEV).' }] : [],
765
- remediations: [{ category: 'vendor_fix', details: validate.selected_remediation?.description || 'See selected remediation path.' }]
766
- })),
807
+ vulnerabilities: [...cveVulns, ...indicatorVulns],
767
808
  exceptd_extension: {
809
+ classification: analyze._detect_classification,
768
810
  rwep: analyze.rwep,
769
811
  blast_radius_score: analyze.blast_radius_score,
770
812
  compliance_theater: analyze.compliance_theater_check,
771
813
  framework_gap_mapping: analyze.framework_gap_mapping,
772
814
  evidence_requirements: validate.evidence_requirements,
773
- residual_risk_statement: validate.residual_risk_statement
815
+ residual_risk_statement: validate.residual_risk_statement,
816
+ indicators_fired: indicatorHits.map(i => ({ id: i.id, confidence: i.confidence, deterministic: i.deterministic })),
774
817
  }
775
818
  };
776
819
  }
777
820
 
778
821
  // SARIF 2.1.0 — GitHub Code Scanning / VS Code SARIF Viewer / Azure DevOps
779
- // and most static analysis tooling. One run per playbook directive, one
780
- // result per matched CVE. Each result references a rule (cve_id) and ties
781
- // back to the directive as the "tool" producer.
822
+ // / most static-analysis tooling.
823
+ //
824
+ // v0.11.5 (#82): emit results from BOTH matched_cves AND fired indicators.
825
+ // Pre-0.11.5 we emitted only matched_cves, which produced an empty bundle
826
+ // for playbooks like crypto-codebase / library-author whose domain.cve_refs
827
+ // is intentionally empty (the playbook checks process/posture, not catalog
828
+ // CVEs). Indicators that fire (verdict: hit) and framework gaps are now
829
+ // first-class SARIF results — a clean run still emits a usable bundle.
782
830
  if (format === 'sarif' || format === 'sarif-2.1.0') {
831
+ const cveResults = analyze.matched_cves.map(c => ({
832
+ ruleId: c.cve_id,
833
+ level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note',
834
+ message: { text: `${c.cve_id}: RWEP ${c.rwep}, blast_radius ${analyze.blast_radius_score}. ${validate.selected_remediation?.description || ''}` },
835
+ properties: {
836
+ kind: 'cve_match',
837
+ rwep: c.rwep, cisa_kev: c.cisa_kev, cisa_kev_due_date: c.cisa_kev_due_date,
838
+ active_exploitation: c.active_exploitation, ai_discovered: c.ai_discovered,
839
+ blast_radius_score: analyze.blast_radius_score,
840
+ }
841
+ }));
842
+ const indicatorHits = (analyze._detect_indicators || []).filter(i => i.verdict === 'hit');
843
+ const indicatorResults = indicatorHits.map(i => ({
844
+ ruleId: i.id,
845
+ level: i.deterministic ? 'error' : (i.confidence === 'high' ? 'warning' : 'note'),
846
+ message: { text: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? ' / deterministic' : ''}). Playbook: ${playbook._meta.id}.` },
847
+ properties: { kind: 'indicator_hit', confidence: i.confidence, deterministic: i.deterministic, atlas_ref: i.atlas_ref, attack_ref: i.attack_ref },
848
+ }));
849
+ const gapResults = (analyze.framework_gap_mapping || []).map((g, idx) => ({
850
+ ruleId: `framework-gap-${idx}`,
851
+ level: 'note',
852
+ message: { text: `${g.framework}: ${g.claimed_control} — ${g.actual_gap}${g.required_control ? '. Required: ' + g.required_control : ''}` },
853
+ properties: { kind: 'framework_gap', framework: g.framework, control: g.claimed_control },
854
+ }));
855
+ const cveRules = analyze.matched_cves.map(c => ({
856
+ id: c.cve_id, shortDescription: { text: c.cve_id },
857
+ fullDescription: { text: `RWEP ${c.rwep} · KEV=${c.cisa_kev} · active_exploitation=${c.active_exploitation}` },
858
+ defaultConfiguration: { level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note' },
859
+ helpUri: `https://nvd.nist.gov/vuln/detail/${c.cve_id}`,
860
+ }));
861
+ const indicatorRules = indicatorHits.map(i => ({
862
+ id: i.id, shortDescription: { text: i.id },
863
+ fullDescription: { text: `Indicator from playbook ${playbook._meta.id}. Type: ${i.type}. Confidence: ${i.confidence}.` },
864
+ defaultConfiguration: { level: i.deterministic ? 'error' : (i.confidence === 'high' ? 'warning' : 'note') },
865
+ }));
783
866
  return {
784
867
  $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
785
868
  version: '2.1.0',
786
869
  runs: [{
787
- tool: {
788
- driver: {
789
- name: 'exceptd',
790
- version: playbook._meta.version,
791
- informationUri: 'https://exceptd.com',
792
- rules: analyze.matched_cves.map(c => ({
793
- id: c.cve_id,
794
- shortDescription: { text: c.cve_id },
795
- fullDescription: { text: `RWEP ${c.rwep} · KEV=${c.cisa_kev} · active_exploitation=${c.active_exploitation} · PoC=${c.poc_available}` },
796
- defaultConfiguration: { level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note' },
797
- helpUri: `https://nvd.nist.gov/vuln/detail/${c.cve_id}`,
798
- }))
799
- }
800
- },
801
- results: analyze.matched_cves.map(c => ({
802
- ruleId: c.cve_id,
803
- level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note',
804
- message: { text: `${c.cve_id}: RWEP ${c.rwep}, blast_radius ${analyze.blast_radius_score}. ${validate.selected_remediation?.description || ''}` },
805
- properties: {
806
- rwep: c.rwep,
807
- cisa_kev: c.cisa_kev,
808
- cisa_kev_due_date: c.cisa_kev_due_date,
809
- active_exploitation: c.active_exploitation,
810
- ai_discovered: c.ai_discovered,
811
- blast_radius_score: analyze.blast_radius_score,
812
- framework_gaps: analyze.framework_gap_mapping?.length || 0,
813
- }
814
- }))
870
+ tool: { driver: {
871
+ name: 'exceptd', version: playbook._meta.version, informationUri: 'https://exceptd.com',
872
+ rules: [...cveRules, ...indicatorRules],
873
+ } },
874
+ results: [...cveResults, ...indicatorResults, ...gapResults],
875
+ invocations: [{ executionSuccessful: true, properties: {
876
+ playbook: playbook._meta.id, classification: analyze._detect_classification || 'unknown',
877
+ rwep_adjusted: analyze.rwep?.adjusted || 0,
878
+ remediation: validate.selected_remediation?.id || null,
879
+ } }],
815
880
  }]
816
881
  };
817
882
  }
818
883
 
819
- // OpenVEX 0.2.0 — supply-chain VEX statements. Each matched CVE becomes a
820
- // statement with status derived from confidence + RWEP. Downstream tools
821
- // (sigstore, in-toto, GUAC) consume this directly.
884
+ // OpenVEX 0.2.0 — supply-chain VEX statements. v0.11.5 (#82): also include
885
+ // statements derived from fired indicators (treated as advisory findings)
886
+ // so playbooks with empty cve_refs still emit a meaningful bundle.
822
887
  if (format === 'openvex' || format === 'openvex-0.2.0') {
823
888
  const issued = new Date().toISOString();
889
+ const cveStatements = analyze.matched_cves.map(c => ({
890
+ vulnerability: { '@id': c.cve_id, name: c.cve_id },
891
+ status: c.active_exploitation === 'confirmed' ? 'under_investigation' : (c.live_patch_available ? 'fixed' : 'affected'),
892
+ timestamp: issued,
893
+ action_statement: validate.selected_remediation?.description || null,
894
+ impact_statement: `RWEP ${c.rwep}. Blast radius ${analyze.blast_radius_score}/5.`
895
+ }));
896
+ const indicatorStatements = (analyze._detect_indicators || []).filter(i => i.verdict === 'hit').map(i => ({
897
+ vulnerability: { '@id': `exceptd:${playbook._meta.id}:${i.id}`, name: i.id },
898
+ status: 'under_investigation',
899
+ timestamp: issued,
900
+ action_statement: validate.selected_remediation?.description || `Run \`exceptd brief ${playbook._meta.id}\` for context.`,
901
+ impact_statement: `Indicator ${i.id} fired (${i.confidence}${i.deterministic ? '/deterministic' : ''}) in playbook ${playbook._meta.id}.`,
902
+ }));
824
903
  return {
825
904
  '@context': 'https://openvex.dev/ns/v0.2.0',
826
905
  '@id': `https://exceptd.com/vex/${playbook._meta.id}/${Date.now()}`,
827
906
  author: 'exceptd',
828
907
  timestamp: issued,
829
908
  version: 1,
830
- statements: analyze.matched_cves.map(c => ({
831
- vulnerability: { '@id': c.cve_id, name: c.cve_id },
832
- status: c.active_exploitation === 'confirmed' ? 'under_investigation' : (c.live_patch_available ? 'fixed' : 'affected'),
833
- timestamp: issued,
834
- action_statement: validate.selected_remediation?.description || null,
835
- impact_statement: `RWEP ${c.rwep}. Blast radius ${analyze.blast_radius_score}/5.`
836
- }))
909
+ statements: [...cveStatements, ...indicatorStatements],
837
910
  };
838
911
  }
839
912
 
@@ -920,6 +993,20 @@ function normalizeSubmission(submission, playbook) {
920
993
  const knownPreconditions = new Set((playbook?._meta?.preconditions || []).map(p => p.id));
921
994
  const knownArtifacts = new Set((playbook?.phases?.look?.artifacts || []).map(a => a.id));
922
995
 
996
+ // v0.11.4 (#71): canonicalize indicator outcome strings here too so the
997
+ // signal_overrides object handed to detect() carries the runner's expected
998
+ // hit|miss|inconclusive vocabulary regardless of what the operator typed.
999
+ const canonicalizeOutcome = (v) => {
1000
+ if (v === true || v === 'hit' || v === 'detected' || v === 'positive') return 'hit';
1001
+ if (v === false || v === 'miss' || v === 'no_hit' || v === 'no-hit' || v === 'clean' || v === 'clear' || v === 'not_hit' || v === 'ok' || v === 'pass' || v === 'negative') return 'miss';
1002
+ if (v === 'inconclusive' || v === 'unknown' || v === 'unverified' || v === null) return 'inconclusive';
1003
+ return v; // leave unrecognized values for detect() to decide
1004
+ };
1005
+
1006
+ // v0.11.5 (#85): track which observation produced each signal_override so
1007
+ // detect can emit `from_observation` on each indicator result. Diagnostic
1008
+ // value for operators chasing "which observation drove this verdict".
1009
+ out._signal_origins = out._signal_origins || {};
923
1010
  for (const [key, val] of Object.entries(submission.observations || {})) {
924
1011
  if (knownPreconditions.has(key)) {
925
1012
  out.precondition_checks[key] = val === "ok" || val === true || val === "true";
@@ -928,7 +1015,10 @@ function normalizeSubmission(submission, playbook) {
928
1015
  if (typeof val === "object" && val !== null) {
929
1016
  const aid = knownArtifacts.has(key) ? key : (val.artifact || key);
930
1017
  out.artifacts[aid] = { value: val.value, captured: val.captured !== false };
931
- if (val.indicator && val.result) out.signal_overrides[val.indicator] = val.result;
1018
+ if (val.indicator && val.result !== undefined) {
1019
+ out.signal_overrides[val.indicator] = canonicalizeOutcome(val.result);
1020
+ out._signal_origins[val.indicator] = key;
1021
+ }
932
1022
  }
933
1023
  }
934
1024
 
@@ -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:45:10.483Z",
3
+ "_generated_at": "2026-05-12T16:58:59.224Z",
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.3",
3
+ "version": "0.11.5",
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",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "last_threat_review": "2026-05-01",
54
54
  "signature": "WprHkO1KOjQtCBj6/EJghBTNyNKJhn7O2HDbAQZPi5jn4flwHpSrtP8LC15a4Unoh+xiIIgGhvTHZIQFHGMpBQ==",
55
- "signed_at": "2026-05-12T15:45:10.018Z",
55
+ "signed_at": "2026-05-12T16:58:58.659Z",
56
56
  "cwe_refs": [
57
57
  "CWE-125",
58
58
  "CWE-362",
@@ -116,7 +116,7 @@
116
116
  ],
117
117
  "last_threat_review": "2026-05-01",
118
118
  "signature": "fg20bOXGRkPUdLmegeXpTM4hnzl/ArgcVc88rItZN5DdsnFnzPgUU1PwCI82zooyj2GfxJHYjxNkq5qd2zNPBg==",
119
- "signed_at": "2026-05-12T15:45:10.020Z",
119
+ "signed_at": "2026-05-12T16:58:58.661Z",
120
120
  "cwe_refs": [
121
121
  "CWE-1039",
122
122
  "CWE-1426",
@@ -179,7 +179,7 @@
179
179
  ],
180
180
  "last_threat_review": "2026-05-01",
181
181
  "signature": "6JuSzkSSFzFHEZ3ANzqjtIbKPOkwJeKhQ+8WAPB4+dTRvDSeg46n3D88XfGaNd2z7pmg/i8p9ZoImQcHFS4BCg==",
182
- "signed_at": "2026-05-12T15:45:10.020Z",
182
+ "signed_at": "2026-05-12T16:58:58.662Z",
183
183
  "cwe_refs": [
184
184
  "CWE-22",
185
185
  "CWE-345",
@@ -225,7 +225,7 @@
225
225
  "framework_gaps": [],
226
226
  "last_threat_review": "2026-05-01",
227
227
  "signature": "PYSw9abiYfW+y7IkY8udJG5LSds2a4rMimlw3rrdD0zE3vunEeV/y7oTmDD4o83OqHSCKNzF/7vMhvd/noqICQ==",
228
- "signed_at": "2026-05-12T15:45:10.021Z"
228
+ "signed_at": "2026-05-12T16:58:58.662Z"
229
229
  },
230
230
  {
231
231
  "name": "compliance-theater",
@@ -256,7 +256,7 @@
256
256
  ],
257
257
  "last_threat_review": "2026-05-01",
258
258
  "signature": "BMFmmJYP3HsHIjUqnhw8E3MiMGZJsI/eDq51we+nxUicZ8nFUQT9DhmRntAqOs6BUnsfiQNNLc/rrsNh8yg1CQ==",
259
- "signed_at": "2026-05-12T15:45:10.021Z"
259
+ "signed_at": "2026-05-12T16:58:58.663Z"
260
260
  },
261
261
  {
262
262
  "name": "exploit-scoring",
@@ -285,7 +285,7 @@
285
285
  ],
286
286
  "last_threat_review": "2026-05-01",
287
287
  "signature": "VGPyDwy5BRlpn1lZthhPB6ytb4ZcU2j0KtCZbaMkyLdMugQJtK2yEuwrsDH4yEtAhTB6/A4B3eSygJckum49Ag==",
288
- "signed_at": "2026-05-12T15:45:10.022Z"
288
+ "signed_at": "2026-05-12T16:58:58.663Z"
289
289
  },
290
290
  {
291
291
  "name": "rag-pipeline-security",
@@ -322,7 +322,7 @@
322
322
  ],
323
323
  "last_threat_review": "2026-05-01",
324
324
  "signature": "XkFGpsNnXBVslkQ48usEu9l1LjPiV2ppW+M4B63zXFBP2Puh52qYCffEPjUHYhoO5bjgTM7yCbK8XF/Dzk5wBw==",
325
- "signed_at": "2026-05-12T15:45:10.022Z",
325
+ "signed_at": "2026-05-12T16:58:58.663Z",
326
326
  "cwe_refs": [
327
327
  "CWE-1395",
328
328
  "CWE-1426"
@@ -379,7 +379,7 @@
379
379
  ],
380
380
  "last_threat_review": "2026-05-01",
381
381
  "signature": "1Xqy7Kxxy6GpTvuYJPdllPzVDRFxb7N6AuxKuoaO4v91CiZLmiXt0sTIWImKJ3p9Eup6rJNDdsY71dolFhHNBA==",
382
- "signed_at": "2026-05-12T15:45:10.022Z",
382
+ "signed_at": "2026-05-12T16:58:58.664Z",
383
383
  "d3fend_refs": [
384
384
  "D3-CA",
385
385
  "D3-CSPP",
@@ -414,7 +414,7 @@
414
414
  "framework_gaps": [],
415
415
  "last_threat_review": "2026-05-01",
416
416
  "signature": "QNLOmAL54S/Cmk4cdO4L2BCGkqZ/FgY4UBsKWtg/EEW+YXF5ev+a8XsUT8q5veuUa2VYcYna7rD1iAnE+2PDBA==",
417
- "signed_at": "2026-05-12T15:45:10.023Z",
417
+ "signed_at": "2026-05-12T16:58:58.664Z",
418
418
  "cwe_refs": [
419
419
  "CWE-1188"
420
420
  ]
@@ -442,7 +442,7 @@
442
442
  "framework_gaps": [],
443
443
  "last_threat_review": "2026-05-01",
444
444
  "signature": "aFHq4cSl3CKchnVITxx+BrAEWD33WtFFJoQtwAug5g9R3/3ABtjaXYGVQaZcdcG1AIZkMoGSPywgLQWDY7ZDCw==",
445
- "signed_at": "2026-05-12T15:45:10.023Z"
445
+ "signed_at": "2026-05-12T16:58:58.664Z"
446
446
  },
447
447
  {
448
448
  "name": "global-grc",
@@ -474,7 +474,7 @@
474
474
  "framework_gaps": [],
475
475
  "last_threat_review": "2026-05-01",
476
476
  "signature": "viCTUWdy6euvd2KTAo6sLvarK/FZkDtYGocxBt0H+fY94kLQGW8K5cSpqIWdUF5NUytSHBCiG4YcSze8P9Z/BQ==",
477
- "signed_at": "2026-05-12T15:45:10.023Z"
477
+ "signed_at": "2026-05-12T16:58:58.665Z"
478
478
  },
479
479
  {
480
480
  "name": "zeroday-gap-learn",
@@ -501,7 +501,7 @@
501
501
  "framework_gaps": [],
502
502
  "last_threat_review": "2026-05-01",
503
503
  "signature": "6PkUaHQi3Hxuqq/Jp4GYckvfqVEofmeT87NUH0T+pwyjlc+xZkoqNPn65f7ldciEPL86JIPi3/dDTKQbIFFBCw==",
504
- "signed_at": "2026-05-12T15:45:10.024Z"
504
+ "signed_at": "2026-05-12T16:58:58.665Z"
505
505
  },
506
506
  {
507
507
  "name": "pqc-first",
@@ -553,7 +553,7 @@
553
553
  ],
554
554
  "last_threat_review": "2026-05-01",
555
555
  "signature": "ZenFTEzWx+DzrSXlNXhbZ70vOdJSXfrnKkAwqMlBf5nlDf38V1/hG4XCKj43snQXWr4mVJOX6ilqFLTYNIjnBw==",
556
- "signed_at": "2026-05-12T15:45:10.024Z",
556
+ "signed_at": "2026-05-12T16:58:58.665Z",
557
557
  "cwe_refs": [
558
558
  "CWE-327"
559
559
  ],
@@ -600,7 +600,7 @@
600
600
  ],
601
601
  "last_threat_review": "2026-05-01",
602
602
  "signature": "ih0vpd2v2zS31JSJv7SnABoya8JlJdrXZXx4rBnrsV3Assj+dbjAP0pQ1HMT/5RX8yTTswRQsg0bJV3qmbJ3Bw==",
603
- "signed_at": "2026-05-12T15:45:10.025Z"
603
+ "signed_at": "2026-05-12T16:58:58.666Z"
604
604
  },
605
605
  {
606
606
  "name": "security-maturity-tiers",
@@ -637,7 +637,7 @@
637
637
  ],
638
638
  "last_threat_review": "2026-05-01",
639
639
  "signature": "Lv8dHiwIqUbNsywCCB/+pYWGF+MHCvxVn1IAvR7Cnif5fy0sICv0N4SVsSb621qAAkHNshpfxqwuhbuQnE1TBA==",
640
- "signed_at": "2026-05-12T15:45:10.025Z",
640
+ "signed_at": "2026-05-12T16:58:58.666Z",
641
641
  "cwe_refs": [
642
642
  "CWE-1188"
643
643
  ]
@@ -672,7 +672,7 @@
672
672
  "framework_gaps": [],
673
673
  "last_threat_review": "2026-05-11",
674
674
  "signature": "BS+wrL28HHYhBpe+v84VLoq9KPBXu6alfG968katfGIoLNYQueaHP931bRmlkrjfeb6qbDf067GWdPEh7nroAw==",
675
- "signed_at": "2026-05-12T15:45:10.025Z"
675
+ "signed_at": "2026-05-12T16:58:58.666Z"
676
676
  },
677
677
  {
678
678
  "name": "attack-surface-pentest",
@@ -743,7 +743,7 @@
743
743
  "PTES revision incorporating AI-surface enumeration"
744
744
  ],
745
745
  "signature": "vLhIYT/CC3IzxMRa+UPeqGSZTvthuwUeTMGNFMm37+TaEk0TtfwPrPyrBJLHw4W6Wt7+pufjHs46X3nTgzoRAg==",
746
- "signed_at": "2026-05-12T15:45:10.025Z"
746
+ "signed_at": "2026-05-12T16:58:58.667Z"
747
747
  },
748
748
  {
749
749
  "name": "fuzz-testing-strategy",
@@ -803,7 +803,7 @@
803
803
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
804
804
  ],
805
805
  "signature": "TOcQLy/427cuf0Lw90J7A0oIeuhUmf9NXb6tOUS5K3SazCKTJujPgYSVAPZOYf1zZrRAY/aq0iqELd5cLyk5DA==",
806
- "signed_at": "2026-05-12T15:45:10.026Z"
806
+ "signed_at": "2026-05-12T16:58:58.667Z"
807
807
  },
808
808
  {
809
809
  "name": "dlp-gap-analysis",
@@ -878,7 +878,7 @@
878
878
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
879
879
  ],
880
880
  "signature": "u4IN7escQa5V+OgdtaJXLdvhmNiGZsdmGOvebTLZ30WoImT+WiksvaqSa0POGdbr6HzFkALe2RrZEH9Tr0U6Dg==",
881
- "signed_at": "2026-05-12T15:45:10.026Z"
881
+ "signed_at": "2026-05-12T16:58:58.667Z"
882
882
  },
883
883
  {
884
884
  "name": "supply-chain-integrity",
@@ -955,7 +955,7 @@
955
955
  "OpenSSF model-signing — emerging Sigstore-based signing standard for ML model weights; track for production adoption"
956
956
  ],
957
957
  "signature": "eTGQJ3gnG24WggfwuFNNIFOWV/ttPxTa3pvx9OH28m5KDS1a4ZmOR7K8y01wk/su8bH0ClYYRfoBfKQOtRswAg==",
958
- "signed_at": "2026-05-12T15:45:10.026Z"
958
+ "signed_at": "2026-05-12T16:58:58.667Z"
959
959
  },
960
960
  {
961
961
  "name": "defensive-countermeasure-mapping",
@@ -1012,7 +1012,7 @@
1012
1012
  ],
1013
1013
  "last_threat_review": "2026-05-11",
1014
1014
  "signature": "q7gFLPoqf/8bqATR6gt/nj0EoyUOlfzi+bZ0bT3pC9KW7O6M/ji9fT+AXSGNp6PKd+70ACb3mkMGmWgjLpQXCg==",
1015
- "signed_at": "2026-05-12T15:45:10.027Z"
1015
+ "signed_at": "2026-05-12T16:58:58.668Z"
1016
1016
  },
1017
1017
  {
1018
1018
  "name": "identity-assurance",
@@ -1079,7 +1079,7 @@
1079
1079
  "d3fend_refs": [],
1080
1080
  "last_threat_review": "2026-05-11",
1081
1081
  "signature": "pX8rhrrzuyG3iRrPORLqTZAjzGdWK/bKPUGJG5WHSZcv4LB0kQXOit4sHG0exdXxI6HY8jyX67QY4r5vEHHACw==",
1082
- "signed_at": "2026-05-12T15:45:10.027Z"
1082
+ "signed_at": "2026-05-12T16:58:58.668Z"
1083
1083
  },
1084
1084
  {
1085
1085
  "name": "ot-ics-security",
@@ -1135,7 +1135,7 @@
1135
1135
  "d3fend_refs": [],
1136
1136
  "last_threat_review": "2026-05-11",
1137
1137
  "signature": "ypb8kNZQRdyu5mWeveB7sjCjNKXS1yXvjDJv88muzwhOs/a4Fu/Gb532js5NKyy+eCw/emrphpTZaL8R9a2lBA==",
1138
- "signed_at": "2026-05-12T15:45:10.027Z"
1138
+ "signed_at": "2026-05-12T16:58:58.668Z"
1139
1139
  },
1140
1140
  {
1141
1141
  "name": "coordinated-vuln-disclosure",
@@ -1187,7 +1187,7 @@
1187
1187
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1188
1188
  ],
1189
1189
  "signature": "346Lt+277ycRNsyAOGwLSONi4awgxKy3hP9G+BWjwaa8ySmTeqbYsbyyhtxjeohk9bV2SF+Hl2q4JdSvc/2qCQ==",
1190
- "signed_at": "2026-05-12T15:45:10.027Z"
1190
+ "signed_at": "2026-05-12T16:58:58.668Z"
1191
1191
  },
1192
1192
  {
1193
1193
  "name": "threat-modeling-methodology",
@@ -1237,7 +1237,7 @@
1237
1237
  "PASTA v2 updates incorporating AI/ML application threats"
1238
1238
  ],
1239
1239
  "signature": "ewTvG5vu3ngFHyXgBur5vSKDFQsOZx0x79djGMricl7LCvQf5//OG6LZKXa+AOuEq58prRS+HgzrFA1DiTfeCQ==",
1240
- "signed_at": "2026-05-12T15:45:10.028Z"
1240
+ "signed_at": "2026-05-12T16:58:58.669Z"
1241
1241
  },
1242
1242
  {
1243
1243
  "name": "webapp-security",
@@ -1311,7 +1311,7 @@
1311
1311
  "d3fend_refs": [],
1312
1312
  "last_threat_review": "2026-05-11",
1313
1313
  "signature": "ZHjbKu0Em92Kimr2esL1g93mf9TmcsChBhVEMWf/lFrjeLcg8nyHEIcDstIZ3FWYgc6MQNHnc3Rup3Xp/Za1Cw==",
1314
- "signed_at": "2026-05-12T15:45:10.028Z"
1314
+ "signed_at": "2026-05-12T16:58:58.669Z"
1315
1315
  },
1316
1316
  {
1317
1317
  "name": "ai-risk-management",
@@ -1361,7 +1361,7 @@
1361
1361
  "d3fend_refs": [],
1362
1362
  "last_threat_review": "2026-05-11",
1363
1363
  "signature": "1KRxjCbAX0Rs5NTOioi1w/f1SOzDQrtRoXjTDtzEwJ+d1QzFf9cqmBlp0uXmGpL0bzEaHWIctjigSychmoL2Dw==",
1364
- "signed_at": "2026-05-12T15:45:10.028Z"
1364
+ "signed_at": "2026-05-12T16:58:58.670Z"
1365
1365
  },
1366
1366
  {
1367
1367
  "name": "sector-healthcare",
@@ -1421,7 +1421,7 @@
1421
1421
  "d3fend_refs": [],
1422
1422
  "last_threat_review": "2026-05-11",
1423
1423
  "signature": "eiajFh7w7d4g+/crGalTtw9Qsu0deVsdHkdthZSy595ifGmgu0zaFD8usKThbPhOdUCCclTYkZYz5GalQmkhCw==",
1424
- "signed_at": "2026-05-12T15:45:10.029Z"
1424
+ "signed_at": "2026-05-12T16:58:58.670Z"
1425
1425
  },
1426
1426
  {
1427
1427
  "name": "sector-financial",
@@ -1502,7 +1502,7 @@
1502
1502
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1503
1503
  ],
1504
1504
  "signature": "iSZR/fYESQVyjkcqj+O+yzU0BQfaELH5s7WizzUTWvDPDTD2ZyOnZTT1r/Zfx2l4mbPmVeFGWdYnnVFTk/i3Aw==",
1505
- "signed_at": "2026-05-12T15:45:10.029Z"
1505
+ "signed_at": "2026-05-12T16:58:58.670Z"
1506
1506
  },
1507
1507
  {
1508
1508
  "name": "sector-federal-government",
@@ -1571,7 +1571,7 @@
1571
1571
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1572
1572
  ],
1573
1573
  "signature": "Wjdo5YXEL8XeNZkaEueG1DOUoyalstNPzQkxD/cwP5iMrJWg/Ly+sC0Oluuqm3aU7d63z55PrbGQCJD0XVZqBg==",
1574
- "signed_at": "2026-05-12T15:45:10.029Z"
1574
+ "signed_at": "2026-05-12T16:58:58.671Z"
1575
1575
  },
1576
1576
  {
1577
1577
  "name": "sector-energy",
@@ -1636,7 +1636,7 @@
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
1638
  "signature": "c/l7dOHe0Zj6Ag3abUaEie6o0f8M4rhY5aPI9/wG4z6FDue9PzCVw8vUGoITFgg89g97lMfy2C3CE2PegQoFCw==",
1639
- "signed_at": "2026-05-12T15:45:10.030Z"
1639
+ "signed_at": "2026-05-12T16:58:58.671Z"
1640
1640
  },
1641
1641
  {
1642
1642
  "name": "api-security",
@@ -1705,7 +1705,7 @@
1705
1705
  "d3fend_refs": [],
1706
1706
  "last_threat_review": "2026-05-11",
1707
1707
  "signature": "9FgcJvYeo07QxQ+mnVRQk4jYLDMO/AVSXMs8cueO2f/qMOTQmrhBMVhj5ze7hzvXpGkp7EK/3Q1XKqde61JMAg==",
1708
- "signed_at": "2026-05-12T15:45:10.030Z"
1708
+ "signed_at": "2026-05-12T16:58:58.671Z"
1709
1709
  },
1710
1710
  {
1711
1711
  "name": "cloud-security",
@@ -1786,7 +1786,7 @@
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
1788
  "signature": "xRA0XZf7VPtuBtbsm41bay9yBLphw/hlL3YxIUrpko5g9ldM3oJe9o1qSwzIj/wSnQSI29qqPpNsnlks+HEOCA==",
1789
- "signed_at": "2026-05-12T15:45:10.030Z"
1789
+ "signed_at": "2026-05-12T16:58:58.672Z"
1790
1790
  },
1791
1791
  {
1792
1792
  "name": "container-runtime-security",
@@ -1848,7 +1848,7 @@
1848
1848
  "d3fend_refs": [],
1849
1849
  "last_threat_review": "2026-05-11",
1850
1850
  "signature": "GcU50DStuN1gU/Evm/sFRgeieQbqffVp12rgbGnasRX89Q7kM4ltFXB+bgCXHIvICzYb78hPIifWQb9UVupWBQ==",
1851
- "signed_at": "2026-05-12T15:45:10.031Z"
1851
+ "signed_at": "2026-05-12T16:58:58.672Z"
1852
1852
  },
1853
1853
  {
1854
1854
  "name": "mlops-security",
@@ -1919,7 +1919,7 @@
1919
1919
  "MITRE ATLAS v5.2 — track AML.T0010 sub-technique expansion and any new MLOps-pipeline-specific TTPs"
1920
1920
  ],
1921
1921
  "signature": "onIazpFoL1t4PMNRsoF06ggnl7BzCKjt0x+ZmVfWfyt1V06DgllsrbN3AAz4+g4jW2Sc71q0vIFKfwEUWpGVAQ==",
1922
- "signed_at": "2026-05-12T15:45:10.031Z"
1922
+ "signed_at": "2026-05-12T16:58:58.672Z"
1923
1923
  },
1924
1924
  {
1925
1925
  "name": "incident-response-playbook",
@@ -1981,7 +1981,7 @@
1981
1981
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
1982
1982
  ],
1983
1983
  "signature": "P0Yv4CtqbnBNP6nSIxQUYYHL7T7ci+iE7iE2UXVfnMPeWVdKG2nvRePjBXc3JZTLima1Txn/I5ocDNhLTIeUAQ==",
1984
- "signed_at": "2026-05-12T15:45:10.031Z"
1984
+ "signed_at": "2026-05-12T16:58:58.673Z"
1985
1985
  },
1986
1986
  {
1987
1987
  "name": "email-security-anti-phishing",
@@ -2034,7 +2034,7 @@
2034
2034
  "d3fend_refs": [],
2035
2035
  "last_threat_review": "2026-05-11",
2036
2036
  "signature": "2pv81lLRbazpHqundCANb3YiLB4lkVsYctIDvI8rxSvHxhPS9jYXqmAoB5APSdDuOaew6XqpfZOehQUj9WmyBw==",
2037
- "signed_at": "2026-05-12T15:45:10.032Z"
2037
+ "signed_at": "2026-05-12T16:58:58.673Z"
2038
2038
  },
2039
2039
  {
2040
2040
  "name": "age-gates-child-safety",
@@ -2102,7 +2102,7 @@
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
2104
  "signature": "BJ/YYnGVXeSBaR9oWAVrcNX7Wz+kE8R4CghX6+XEI/qY89fyrkKNNwo2veqqf49wffJhHVJ1wTp8ZDECjNp+Dw==",
2105
- "signed_at": "2026-05-12T15:45:10.032Z"
2105
+ "signed_at": "2026-05-12T16:58:58.673Z"
2106
2106
  }
2107
2107
  ]
2108
2108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
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",
package/sbom.cdx.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:bc8a18f6-0f9a-4701-9673-d234acd23435",
4
+ "serialNumber": "urn:uuid:396b0919-1153-402c-9438-12692eb441ce",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-12T15:45:10.912Z",
7
+ "timestamp": "2026-05-12T16:58:59.686Z",
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.3",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.11.5",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.11.3",
19
+ "version": "0.11.5",
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.3",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.11.5",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.3"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.5"
33
33
  },
34
34
  {
35
35
  "type": "vcs",