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