@blamejs/exceptd-skills 0.11.4 → 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,35 @@
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
+
3
33
  ## 0.11.4 — 2026-05-12
4
34
 
5
35
  **Patch: high-impact #71 fix + items 72-77.**
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
  }
@@ -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:00:14.346Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "6935c71303fe299a6a52e1776d571e30808cae14ddfadc48b27d9ac2443c423a",
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",
@@ -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,16 +772,30 @@ 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
+ }));
764
793
  return {
765
794
  document: {
766
795
  category: 'csaf_security_advisory',
767
796
  csaf_version: '2.0',
768
797
  publisher: { category: 'vendor', name: 'exceptd', namespace: 'https://exceptd.com' },
769
- 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))`,
770
799
  tracking: {
771
800
  id: `exceptd-${playbook._meta.id}-${Date.now()}`,
772
801
  status: 'final',
@@ -775,82 +804,109 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
775
804
  revision_history: [{ number: '1', date: new Date().toISOString(), summary: 'Initial finding emission' }]
776
805
  }
777
806
  },
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
- })),
807
+ vulnerabilities: [...cveVulns, ...indicatorVulns],
784
808
  exceptd_extension: {
809
+ classification: analyze._detect_classification,
785
810
  rwep: analyze.rwep,
786
811
  blast_radius_score: analyze.blast_radius_score,
787
812
  compliance_theater: analyze.compliance_theater_check,
788
813
  framework_gap_mapping: analyze.framework_gap_mapping,
789
814
  evidence_requirements: validate.evidence_requirements,
790
- 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 })),
791
817
  }
792
818
  };
793
819
  }
794
820
 
795
821
  // 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.
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.
799
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
+ }));
800
866
  return {
801
867
  $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
802
868
  version: '2.1.0',
803
869
  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
- }))
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
+ } }],
832
880
  }]
833
881
  };
834
882
  }
835
883
 
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.
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.
839
887
  if (format === 'openvex' || format === 'openvex-0.2.0') {
840
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
+ }));
841
903
  return {
842
904
  '@context': 'https://openvex.dev/ns/v0.2.0',
843
905
  '@id': `https://exceptd.com/vex/${playbook._meta.id}/${Date.now()}`,
844
906
  author: 'exceptd',
845
907
  timestamp: issued,
846
908
  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
- }))
909
+ statements: [...cveStatements, ...indicatorStatements],
854
910
  };
855
911
  }
856
912
 
@@ -947,6 +1003,10 @@ function normalizeSubmission(submission, playbook) {
947
1003
  return v; // leave unrecognized values for detect() to decide
948
1004
  };
949
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 || {};
950
1010
  for (const [key, val] of Object.entries(submission.observations || {})) {
951
1011
  if (knownPreconditions.has(key)) {
952
1012
  out.precondition_checks[key] = val === "ok" || val === true || val === "true";
@@ -957,6 +1017,7 @@ function normalizeSubmission(submission, playbook) {
957
1017
  out.artifacts[aid] = { value: val.value, captured: val.captured !== false };
958
1018
  if (val.indicator && val.result !== undefined) {
959
1019
  out.signal_overrides[val.indicator] = canonicalizeOutcome(val.result);
1020
+ out._signal_origins[val.indicator] = key;
960
1021
  }
961
1022
  }
962
1023
  }
@@ -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-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.4",
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:59:29.388Z",
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:59:29.390Z",
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:59:29.390Z",
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:59:29.391Z"
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:59:29.392Z"
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:59:29.392Z"
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:59:29.392Z",
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:59:29.392Z",
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:59:29.393Z",
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:59:29.393Z"
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:59:29.394Z"
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:59:29.394Z"
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:59:29.394Z",
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:59:29.395Z"
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:59:29.395Z",
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:59:29.395Z"
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:59:29.396Z"
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:59:29.396Z"
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:59:29.396Z"
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:59:29.396Z"
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:59:29.397Z"
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:59:29.397Z"
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:59:29.397Z"
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:59:29.398Z"
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:59:29.398Z"
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:59:29.398Z"
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:59:29.399Z"
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:59:29.399Z"
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:59:29.399Z"
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:59:29.400Z"
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:59:29.400Z"
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:59:29.400Z"
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:59:29.401Z"
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:59:29.401Z"
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:59:29.401Z"
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:59:29.402Z"
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:59:29.402Z"
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:59:29.402Z"
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.4",
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:057eee67-86e4-4750-80f8-8dfdf5b5c9d3",
4
+ "serialNumber": "urn:uuid:396b0919-1153-402c-9438-12692eb441ce",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-12T15:59:30.308Z",
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.4",
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.4",
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.4",
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.4"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.11.5"
33
33
  },
34
34
  {
35
35
  "type": "vcs",