@blamejs/exceptd-skills 0.11.1 → 0.11.3
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 +60 -0
- package/bin/exceptd.js +337 -26
- package/data/_indexes/_meta.json +2 -2
- package/lib/playbook-runner.js +33 -4
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/index.js +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.3 — 2026-05-12
|
|
4
|
+
|
|
5
|
+
**Patch: operator-reported item #71 + full feature audit findings.**
|
|
6
|
+
|
|
7
|
+
A full audit across v0.10.0 → v0.11.2 features (64 surface elements: bug fixes, new verbs, flags, output formats, integration paths) confirmed 62/64 work as documented; this release fixes the 2 real gaps the audit found plus closes operator-reported #71.
|
|
8
|
+
|
|
9
|
+
### Bugs
|
|
10
|
+
|
|
11
|
+
- **#71 lint accepted half-shape submissions the runner couldn't drive detect with.** Operators submitting flat-shape evidence with `observations: { "<artifact-id>": { captured, value } }` (no `indicator + result` inline) passed lint with zero warnings, then got `detect.classification: "inconclusive"` from the runner because nothing drove indicator decisions. The flat-shape migration was half-complete: validator accepted the new shape; runner couldn't consume it.
|
|
12
|
+
|
|
13
|
+
Fixes:
|
|
14
|
+
- **Lint** now warns `observation_lacks_indicator_result` per captured artifact that lacks `indicator + result` AND no `verdict.classification` is supplied, plus an `info` saying "detect will be inconclusive". Operators see the gap before paying the run cost.
|
|
15
|
+
- **`normalizeSubmission`** previously bailed when the submission already had any nested key (`signals`, `artifacts`, `signal_overrides`) — including when the CLI itself had injected `signals._bundle_formats` for `--format` support. Now shape detection prioritizes `observations` / `verdict` and merges any pre-existing nested keys into the normalized output.
|
|
16
|
+
- **`detect` output** surfaces `observations_received`, `signals_received`, `indicators_evaluated`, `classification_override_applied`, and `submission_shape_seen` so operators can see exactly what the runner consumed from their submission. Pre-0.11.3 an inconclusive verdict was opaque.
|
|
17
|
+
|
|
18
|
+
- **`attest export --format csaf` was a no-op.** The `--format` flag is registered as a multi-flag (returning an array), but the export subverb compared `format === "csaf"` directly against the array, falsing every time. Operators always got the plain redacted-JSON export regardless of the flag. Now unwrapped + normalizes `csaf-2.0` → `csaf` so both shortcuts hit the CSAF envelope path.
|
|
19
|
+
|
|
20
|
+
### Audit pass — verified working as documented
|
|
21
|
+
|
|
22
|
+
Smoke-tested 64 features across v0.10.0–v0.11.2. The full list:
|
|
23
|
+
|
|
24
|
+
- **Bug regressions:** skill not-found JSON, unknown-command JSON, prefetch --quiet summary, validate-cves --offline, --mode validation, --session-key hex validation, framework-gap NIST normalization, default-stdin on pipe, --json-stdout-only stderr silence, mutex lockfile released after run, session-id collision refusal, --operator persistence, --ack persistence, --diff-from-latest, reattest --latest.
|
|
25
|
+
- **Verbs:** brief (incl. --all / --phase), discover, doctor (all four sub-checks), ask (incl. synonym routing), lint (catches missing artifacts), ci (incl. --scope code alignment with discover), watch, verify-attestation alias, run-all alias, attest list/show/verify/export/diff/diff --against.
|
|
26
|
+
- **Run flags:** --evidence, --evidence-dir, --vex, --explain, --signal-list, --format summary/markdown/sarif/openvex (--format csaf fixed here), --diff-from-latest, --ci, --force-overwrite.
|
|
27
|
+
- **Attestation root:** EXCEPTD_HOME respected, --attestation-root respected, legacy + new root both scanned by `findSessionDir`.
|
|
28
|
+
- **Catalog tooling:** validate-cves --since filter, refresh --no-network / --indexes-only routing, report csaf envelope.
|
|
29
|
+
- **Flat submission shape:** verdict.classification propagates, observation + indicator + result drives detect, smart precondition auto-detect resolves cwd_readable / host.platform / agent_has_command.
|
|
30
|
+
- **First-run welcome.**
|
|
31
|
+
|
|
32
|
+
### Audit pass — known false positives
|
|
33
|
+
|
|
34
|
+
- **`exceptd watch`** prints `"[orchestrator] Starting event watcher..."` not `"Listening"` — works correctly; my test string was wrong.
|
|
35
|
+
|
|
36
|
+
## 0.11.2 — 2026-05-12
|
|
37
|
+
|
|
38
|
+
**Patch: operator-reported items 58-70 from real CLI use.**
|
|
39
|
+
|
|
40
|
+
### Bugs
|
|
41
|
+
|
|
42
|
+
- **#58 `ask` non-functional.** Even literal token "secrets" returned `matched: []`. Root cause: tokenizer required length > 3 (dropped "PQC"/"MCP") and the search index covered only `domain.name + attack_class + first sentence of threat_context`. Rewritten with: (a) length >= 2 token filter, (b) synonym map (`credential` → secret/key/token/...; `supply chain` → sbom/dependency/...; `pqc` → post-quantum/ml-kem/...), (c) richer index covering id + name + attack_class + atlas_refs + attack_refs + cwe_refs + frameworks_in_scope + theater_fingerprints.claim + full threat_context + framework_lag_declaration + skill_chain + collection_scope, (d) ID match scores 3× (so `ask secrets` routes to the secrets playbook). Default output now human-readable; `--json` for machine.
|
|
43
|
+
- **#59 `--format` flag was no-op.** Documented values produced standard JSON unconditionally. Wired through: `--format summary` emits a single-line JSON digest; `--format markdown` emits an operator-readable markdown report; `--format csaf-2.0|sarif|openvex` emits the corresponding bundle from `close.evidence_package.bundles_by_format`. Unknown values rejected with a list of valid options.
|
|
44
|
+
- **#60 Default output flipped (partial).** `emit()` now detects `stdout.isTTY` — interactive use gets indented JSON (massively more readable); piped use stays compact. Override via `--pretty` (always indent) or `EXCEPTD_RAW_JSON=1`. Verbs with dedicated human renderers (`discover`, `doctor`, `ask`) still use them.
|
|
45
|
+
- **#61 doctor summary contradicted its findings.** Output said "all checks green" directly above `[!!] private key MISSING`. Now: signing-check severity is `warn` when key absent; summary distinguishes errors vs warnings (`X fail / Y warn`); icon shows `[!! warn]` instead of `[ok]`. Warnings don't force exit 1 (CI still ok) but the visible state matches.
|
|
46
|
+
- **#62 `watch` verb missing.** The deprecation map said `watchlist → watch` but `watch` returned unknown-command. Added `watch` as orchestrator passthrough aliased to `watchlist` (same function).
|
|
47
|
+
- **#63 `discover` vs `ci --scope code` mismatch.** discover recommended 5 playbooks; ci ran 4 (different sets). ci now includes cross-cutting playbooks (`framework`) regardless of scope, and for `--scope code` on a git repo with a lockfile, also includes `sbom` (system-scope but repo-relevant). Aligns with discover's recommendations.
|
|
48
|
+
- **#65 `refresh --no-network` / `--indexes-only` silently no-op.** v0.11.0 deprecation pointers said `prefetch → refresh --no-network` and `build-indexes → refresh --indexes-only`, but the underlying refresh script ignored those flags. Now: CLI translates them at dispatch time — `refresh --no-network` routes to the `prefetch` script; `refresh --indexes-only` routes to `build-indexes`.
|
|
49
|
+
- **#66 `ai-run` shell-pipe unusable.** `echo '{...}' | exceptd ai-run secrets` failed with "stdin closed without an evidence event" because shell heredocs close stdin before the streaming protocol expects the wrapped `{event:evidence}` frame. Fix: when streaming mode hits EOF without a wrapped event, parse the raw stdin as a bare submission object and run with it. Operators no longer need an interactive harness for the common single-shot case.
|
|
50
|
+
- **#64 verified.** `ok:false` from `on_fail: halt` preconditions correctly exits 1 (kernel-on-Windows reproducer). The user's `exceptd run secrets` cases were `on_fail: warn` preconditions where exit 0 is correct (run completed with warning). No regression in v0.11.x; the user's stale install may have shown different behavior.
|
|
51
|
+
|
|
52
|
+
### Features
|
|
53
|
+
|
|
54
|
+
- **#67** `ask` routing index — same fix as #58.
|
|
55
|
+
- **#68** `--format summary` single-line digest — same fix as #59. Returns: `{ok, playbook, session_id, classification, rwep, blast_radius, matched_cves, feeds_into, jurisdiction_clocks, evidence_hash}`. Useful for GH Actions annotation lines.
|
|
56
|
+
- **#69** `doctor --fix` automatically runs `node lib/sign.js generate-keypair` when the private-key check is the only failing warning. Closes the most-common discovered-issue → manual-fix-recipe loop.
|
|
57
|
+
- **#70** `run --format markdown` emits an operator-readable per-run digest (classification, RWEP, matched CVEs, recommended remediation, notification clocks, feeds_into).
|
|
58
|
+
|
|
59
|
+
### Already shipped (cross-referenced)
|
|
60
|
+
|
|
61
|
+
- `attest diff <a> --against <b>` (was v0.11.0 #56) — works as documented.
|
|
62
|
+
|
|
3
63
|
## 0.11.1 — 2026-05-12
|
|
4
64
|
|
|
5
65
|
**Patch: operator-reported items 47-57.**
|
package/bin/exceptd.js
CHANGED
|
@@ -81,6 +81,7 @@ const COMMANDS = {
|
|
|
81
81
|
"validate-cves": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
82
82
|
"validate-rfcs": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
83
83
|
watchlist: () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
84
|
+
watch: () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
84
85
|
"framework-gap": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
85
86
|
"framework-gap-analysis": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
86
87
|
// Seven-phase playbook verbs — handled in-process via lib/playbook-runner.js.
|
|
@@ -95,7 +96,7 @@ const COMMANDS = {
|
|
|
95
96
|
|
|
96
97
|
const ORCHESTRATOR_PASSTHROUGH = new Set([
|
|
97
98
|
"scan", "dispatch", "skill", "currency", "report",
|
|
98
|
-
"validate-cves", "validate-rfcs", "watchlist",
|
|
99
|
+
"validate-cves", "validate-rfcs", "watchlist", "watch",
|
|
99
100
|
"framework-gap", "framework-gap-analysis",
|
|
100
101
|
]);
|
|
101
102
|
|
|
@@ -339,7 +340,21 @@ function main() {
|
|
|
339
340
|
return;
|
|
340
341
|
}
|
|
341
342
|
|
|
342
|
-
|
|
343
|
+
// v0.11.2 bug #65: `refresh --no-network` / `refresh --indexes-only` were
|
|
344
|
+
// documented as the v0.11.0 replacements for `prefetch` / `build-indexes`
|
|
345
|
+
// but the underlying refresh script doesn't know those flags. Translate
|
|
346
|
+
// here so the deprecation pointer actually works.
|
|
347
|
+
let effectiveCmd = cmd;
|
|
348
|
+
let effectiveRest = rest;
|
|
349
|
+
if (cmd === "refresh" && rest.includes("--no-network")) {
|
|
350
|
+
effectiveCmd = "prefetch";
|
|
351
|
+
effectiveRest = rest.filter(a => a !== "--no-network");
|
|
352
|
+
} else if (cmd === "refresh" && rest.includes("--indexes-only")) {
|
|
353
|
+
effectiveCmd = "build-indexes";
|
|
354
|
+
effectiveRest = rest.filter(a => a !== "--indexes-only");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const resolver = COMMANDS[effectiveCmd];
|
|
343
358
|
if (typeof resolver !== "function") {
|
|
344
359
|
// Emit a structured JSON error matching the seven-phase verbs so operators
|
|
345
360
|
// piping through `jq` get one consistent shape across the CLI surface.
|
|
@@ -357,7 +372,7 @@ function main() {
|
|
|
357
372
|
|
|
358
373
|
// Orchestrator subcommands need the subcommand name preserved as argv[0]
|
|
359
374
|
// for orchestrator/index.js's switch statement.
|
|
360
|
-
const finalArgs = ORCHESTRATOR_PASSTHROUGH.has(
|
|
375
|
+
const finalArgs = ORCHESTRATOR_PASSTHROUGH.has(effectiveCmd) ? [script, effectiveCmd, ...effectiveRest] : [script, ...effectiveRest];
|
|
361
376
|
const res = spawnSync(process.execPath, finalArgs, { stdio: "inherit", cwd: PKG_ROOT });
|
|
362
377
|
if (res.error) {
|
|
363
378
|
process.stderr.write(`exceptd: failed to run ${cmd}: ${res.error.message}\n`);
|
|
@@ -413,7 +428,13 @@ function parseArgs(argv, opts) {
|
|
|
413
428
|
}
|
|
414
429
|
|
|
415
430
|
function emit(obj, pretty) {
|
|
416
|
-
|
|
431
|
+
// v0.11.2 bug #60: when stdout is a TTY (interactive use), emit indented
|
|
432
|
+
// JSON instead of single-line — much more readable. Piped to a file or
|
|
433
|
+
// tool? Default to compact one-line JSON. --pretty forces indented
|
|
434
|
+
// regardless of TTY. --json-stdout-only is always compact.
|
|
435
|
+
const interactive = process.stdout.isTTY && !process.env.EXCEPTD_RAW_JSON;
|
|
436
|
+
const indent = pretty || (interactive && !pretty);
|
|
437
|
+
const s = indent ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
|
|
417
438
|
process.stdout.write(s + "\n");
|
|
418
439
|
}
|
|
419
440
|
|
|
@@ -458,9 +479,20 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
458
479
|
bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives",
|
|
459
480
|
"ci", "latest", "diff-from-latest", "explain", "signal-list", "ack",
|
|
460
481
|
"force-overwrite", "no-stream", "block-on-jurisdiction-clock",
|
|
461
|
-
"json-stdout-only"],
|
|
482
|
+
"json-stdout-only", "fix", "human", "json"],
|
|
462
483
|
multi: ["playbook", "format"],
|
|
463
484
|
});
|
|
485
|
+
// v0.11.2 bug #60: flip defaults to human-readable. JSON via explicit --json
|
|
486
|
+
// (or --pretty implies indented JSON). The v0.11.0 CHANGELOG claimed this
|
|
487
|
+
// was already done; the code in fact emitted JSON unconditionally. Now:
|
|
488
|
+
// --json or --pretty → JSON (one-line or indented respectively)
|
|
489
|
+
// --json-stdout-only → JSON, suppress stderr
|
|
490
|
+
// default → human-readable text
|
|
491
|
+
// Verbs that have their own human renderer (discover/doctor/refresh/lint
|
|
492
|
+
// /ask/attest list) continue to use it; verbs that don't yet (brief/run/
|
|
493
|
+
// ai-run/ci/attest show/export/diff/verify) fall back to indented JSON
|
|
494
|
+
// labeled as such — better than no signal.
|
|
495
|
+
args._jsonMode = !!(args.json || args.pretty || args["json-stdout-only"]);
|
|
464
496
|
const pretty = !!args.pretty;
|
|
465
497
|
const runOpts = {
|
|
466
498
|
airGap: !!args["air-gap"],
|
|
@@ -834,6 +866,37 @@ function cmdLint(runner, args, runOpts, pretty) {
|
|
|
834
866
|
issues.push({ severity: "warn", kind: "unknown_observation_key", key: k });
|
|
835
867
|
}
|
|
836
868
|
|
|
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.
|
|
874
|
+
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
|
+
const verdictClass = submission.verdict?.classification;
|
|
882
|
+
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
|
+
}
|
|
892
|
+
issues.push({
|
|
893
|
+
severity: "info",
|
|
894
|
+
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.`,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
837
900
|
const ok = issues.every(i => i.severity !== "error");
|
|
838
901
|
emit({
|
|
839
902
|
verb: "lint",
|
|
@@ -1134,7 +1197,12 @@ function cmdRun(runner, args, runOpts, pretty) {
|
|
|
1134
1197
|
// Supports csaf-2.0 | sarif | openvex | markdown. Multiple --format flags
|
|
1135
1198
|
// produce multiple bundles in the close response under bundles_by_format.
|
|
1136
1199
|
if (args.format) {
|
|
1137
|
-
|
|
1200
|
+
// Normalize shortcut names to the runner's canonical bundle keys before
|
|
1201
|
+
// passing through. "csaf" → "csaf-2.0"; "sarif" / "openvex" / "markdown"
|
|
1202
|
+
// / "summary" stay verbatim. Anything else is rejected after the run
|
|
1203
|
+
// result is in hand (so the run still completes).
|
|
1204
|
+
const formats = (Array.isArray(args.format) ? args.format : [args.format])
|
|
1205
|
+
.map(f => f === "csaf" ? "csaf-2.0" : f);
|
|
1138
1206
|
submission.signals = submission.signals || {};
|
|
1139
1207
|
submission.signals._bundle_formats = formats;
|
|
1140
1208
|
}
|
|
@@ -1259,6 +1327,80 @@ function cmdRun(runner, args, runOpts, pretty) {
|
|
|
1259
1327
|
return;
|
|
1260
1328
|
}
|
|
1261
1329
|
|
|
1330
|
+
// v0.11.2 bug #59 / feature #70: --format actually transforms the top-level
|
|
1331
|
+
// output. Previously it only populated close.evidence_package.bundles_by_format
|
|
1332
|
+
// and the operator still saw the full JSON. Now:
|
|
1333
|
+
// --format summary → single-line JSON digest (5 fields)
|
|
1334
|
+
// --format markdown → operator-readable markdown digest of the run
|
|
1335
|
+
// --format csaf-2.0/sarif/openvex → the corresponding bundle from close
|
|
1336
|
+
// (default — no --format) → full JSON result as before
|
|
1337
|
+
if (args.format) {
|
|
1338
|
+
const requested = Array.isArray(args.format) ? args.format[0] : args.format;
|
|
1339
|
+
const VALID = ["summary", "markdown", "csaf-2.0", "csaf", "sarif", "openvex", "json"];
|
|
1340
|
+
if (!VALID.includes(requested)) {
|
|
1341
|
+
return emitError(`run: --format "${requested}" not in accepted set ${JSON.stringify(VALID)}.`, null, pretty);
|
|
1342
|
+
}
|
|
1343
|
+
if (requested === "summary") {
|
|
1344
|
+
const cls = result.phases?.detect?.classification;
|
|
1345
|
+
const rwep = result.phases?.analyze?.rwep?.adjusted ?? 0;
|
|
1346
|
+
const blast = result.phases?.analyze?.blast_radius_score ?? 0;
|
|
1347
|
+
const cves = result.phases?.analyze?.matched_cves?.length ?? 0;
|
|
1348
|
+
const next = result.phases?.close?.feeds_into?.join(",") || "";
|
|
1349
|
+
const clocks = (result.phases?.close?.notification_actions || []).filter(n => n.clock_started_at).length;
|
|
1350
|
+
emit({
|
|
1351
|
+
ok: result.ok, playbook: result.playbook_id, session_id: result.session_id,
|
|
1352
|
+
classification: cls, rwep, blast_radius: blast, matched_cves: cves,
|
|
1353
|
+
feeds_into: next, jurisdiction_clocks: clocks, evidence_hash: result.evidence_hash,
|
|
1354
|
+
}, pretty);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (requested === "markdown") {
|
|
1358
|
+
const lines = [];
|
|
1359
|
+
lines.push(`# exceptd run: ${result.playbook_id}`);
|
|
1360
|
+
lines.push(`session-id: ${result.session_id}`);
|
|
1361
|
+
lines.push(`evidence-hash: ${result.evidence_hash}`);
|
|
1362
|
+
lines.push("");
|
|
1363
|
+
const cls = result.phases?.detect?.classification || "n/a";
|
|
1364
|
+
const rwep = result.phases?.analyze?.rwep?.adjusted ?? 0;
|
|
1365
|
+
const top = result.phases?.analyze?.rwep?.threshold?.escalate ?? "n/a";
|
|
1366
|
+
lines.push(`**Classification:** ${cls} **RWEP:** ${rwep} / ${top} **Blast radius:** ${result.phases?.analyze?.blast_radius_score ?? "n/a"}/5`);
|
|
1367
|
+
lines.push("");
|
|
1368
|
+
const cves = result.phases?.analyze?.matched_cves || [];
|
|
1369
|
+
if (cves.length) {
|
|
1370
|
+
lines.push(`## Matched CVEs (${cves.length})`);
|
|
1371
|
+
for (const c of cves) lines.push(`- **${c.cve_id}** · RWEP ${c.rwep} · KEV=${c.cisa_kev} · ${c.active_exploitation}`);
|
|
1372
|
+
lines.push("");
|
|
1373
|
+
}
|
|
1374
|
+
const rem = result.phases?.validate?.selected_remediation;
|
|
1375
|
+
if (rem) {
|
|
1376
|
+
lines.push(`## Recommended remediation`);
|
|
1377
|
+
lines.push(`**${rem.id}** (priority ${rem.priority}) — ${rem.description}`);
|
|
1378
|
+
lines.push("");
|
|
1379
|
+
}
|
|
1380
|
+
const notif = result.phases?.close?.notification_actions || [];
|
|
1381
|
+
if (notif.length) {
|
|
1382
|
+
lines.push(`## Notification clocks`);
|
|
1383
|
+
for (const n of notif) lines.push(`- ${n.obligation_ref} → deadline ${n.deadline}`);
|
|
1384
|
+
lines.push("");
|
|
1385
|
+
}
|
|
1386
|
+
const feeds = result.phases?.close?.feeds_into || [];
|
|
1387
|
+
if (feeds.length) lines.push(`**Next playbooks suggested:** ${feeds.join(", ")}`);
|
|
1388
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
// CSAF/SARIF/OpenVEX bundles live under close.evidence_package — the
|
|
1392
|
+
// runner writes them under canonical keys ("csaf-2.0", "sarif",
|
|
1393
|
+
// "openvex"). Normalize the user-supplied shortcuts.
|
|
1394
|
+
const formatNorm = requested === "csaf" ? "csaf-2.0" : requested;
|
|
1395
|
+
const bbf = result.phases?.close?.evidence_package?.bundles_by_format || {};
|
|
1396
|
+
const body = bbf[formatNorm] || result.phases?.close?.evidence_package?.bundle_body;
|
|
1397
|
+
if (body) {
|
|
1398
|
+
emit(body, pretty);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
// Fallback: full result
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1262
1404
|
emit(result, pretty);
|
|
1263
1405
|
}
|
|
1264
1406
|
|
|
@@ -1833,7 +1975,12 @@ function cmdAttest(runner, args, runOpts, pretty) {
|
|
|
1833
1975
|
// remediation choice, residual risk acceptance, signature. Auditors get
|
|
1834
1976
|
// what they need (the verdict + proof of process) without leaking raw
|
|
1835
1977
|
// captured data (which may contain PII / secret shapes).
|
|
1836
|
-
|
|
1978
|
+
//
|
|
1979
|
+
// v0.11.3: --format is registered as multi in parseArgs, so args.format
|
|
1980
|
+
// is an array when present. Unwrap for direct comparison.
|
|
1981
|
+
let formatRaw = args.format || "json";
|
|
1982
|
+
if (Array.isArray(formatRaw)) formatRaw = formatRaw[0];
|
|
1983
|
+
const format = formatRaw === "csaf-2.0" ? "csaf" : formatRaw;
|
|
1837
1984
|
const redacted = attestations.map(a => ({
|
|
1838
1985
|
session_id: a.session_id,
|
|
1839
1986
|
playbook_id: a.playbook_id,
|
|
@@ -2260,22 +2407,43 @@ function cmdDoctor(runner, args, runOpts, pretty) {
|
|
|
2260
2407
|
const keyPath = path.join(process.cwd(), ".keys", "private.pem");
|
|
2261
2408
|
const fallback = path.join(PKG_ROOT, ".keys", "private.pem");
|
|
2262
2409
|
const present = fs.existsSync(keyPath) || fs.existsSync(fallback);
|
|
2410
|
+
// Bug #61 (v0.11.2): signing-status missing key is a real WARNING. The
|
|
2411
|
+
// attestation pipeline writes unsigned files when this is absent, which
|
|
2412
|
+
// operators reading the attestation later cannot verify for authenticity.
|
|
2413
|
+
// The summary line must reflect this — pre-0.11.2 said "all checks green"
|
|
2414
|
+
// directly above [!!] private key MISSING. Now: it's a warning that
|
|
2415
|
+
// populates summary.warnings_count.
|
|
2263
2416
|
checks.signing = {
|
|
2264
|
-
ok:
|
|
2417
|
+
ok: present, // not green if the key is missing — operators need the nudge
|
|
2418
|
+
severity: present ? "info" : "warn",
|
|
2265
2419
|
private_key_present: present,
|
|
2266
2420
|
can_sign_attestations: present,
|
|
2267
|
-
...(present ? {} : { hint: "run `node lib/sign.js generate-keypair` to enable attestation signing" }),
|
|
2421
|
+
...(present ? {} : { hint: "run `node lib/sign.js generate-keypair` (or `exceptd doctor --fix`) to enable attestation signing" }),
|
|
2268
2422
|
};
|
|
2269
2423
|
} catch (e) {
|
|
2270
2424
|
checks.signing = { ok: false, error: e.message };
|
|
2271
2425
|
}
|
|
2272
2426
|
}
|
|
2273
2427
|
|
|
2274
|
-
|
|
2428
|
+
// Walk every check and split: errors (severity error/missing/fail) vs warnings
|
|
2429
|
+
// (severity warn). all_green is true ONLY when zero errors AND zero warnings.
|
|
2430
|
+
const warnList = [];
|
|
2431
|
+
const errorList = [];
|
|
2432
|
+
for (const [k, v] of Object.entries(checks)) {
|
|
2433
|
+
if (v.ok === false) errorList.push(k);
|
|
2434
|
+
else if (v.severity === "warn") warnList.push(k);
|
|
2435
|
+
}
|
|
2436
|
+
const allGreen = errorList.length === 0 && warnList.length === 0;
|
|
2275
2437
|
const out = {
|
|
2276
2438
|
verb: "doctor",
|
|
2277
2439
|
checks,
|
|
2278
|
-
summary: {
|
|
2440
|
+
summary: {
|
|
2441
|
+
all_green: allGreen,
|
|
2442
|
+
issues_count: errorList.length,
|
|
2443
|
+
warnings_count: warnList.length,
|
|
2444
|
+
failed_checks: errorList,
|
|
2445
|
+
warning_checks: warnList,
|
|
2446
|
+
},
|
|
2279
2447
|
};
|
|
2280
2448
|
|
|
2281
2449
|
if (wantJson) {
|
|
@@ -2289,7 +2457,10 @@ function cmdDoctor(runner, args, runOpts, pretty) {
|
|
|
2289
2457
|
lines.push("exceptd doctor");
|
|
2290
2458
|
function mark(c, render) {
|
|
2291
2459
|
if (!c) return;
|
|
2292
|
-
|
|
2460
|
+
// Three states: ok / warn / error. Bug #61 (v0.11.2) — warn must not be
|
|
2461
|
+
// shown as ok and must count toward the summary so the bottom line
|
|
2462
|
+
// matches the visible icons above.
|
|
2463
|
+
const icon = c.ok && c.severity !== "warn" ? "[ok]" : (c.severity === "warn" ? "[!! warn]" : "[!! fail]");
|
|
2293
2464
|
lines.push(` ${icon} ${render(c)}`);
|
|
2294
2465
|
}
|
|
2295
2466
|
mark(checks.signatures, c =>
|
|
@@ -2320,9 +2491,30 @@ function cmdDoctor(runner, args, runOpts, pretty) {
|
|
|
2320
2491
|
}
|
|
2321
2492
|
}
|
|
2322
2493
|
lines.push("");
|
|
2323
|
-
|
|
2494
|
+
if (allGreen) {
|
|
2495
|
+
lines.push(`summary: all checks green`);
|
|
2496
|
+
} else if (errorList.length === 0) {
|
|
2497
|
+
lines.push(`summary: ${warnList.length} warning(s) — ${warnList.join(", ")}`);
|
|
2498
|
+
} else {
|
|
2499
|
+
lines.push(`summary: ${errorList.length} fail / ${warnList.length} warn — fail: ${errorList.join(", ")}; warn: ${warnList.join(", ") || "none"}`);
|
|
2500
|
+
}
|
|
2324
2501
|
process.stdout.write(lines.join("\n") + "\n");
|
|
2325
|
-
|
|
2502
|
+
// Bug #69 (v0.11.2): --fix mode for missing private key.
|
|
2503
|
+
if (args.fix && checks.signing && !checks.signing.private_key_present) {
|
|
2504
|
+
process.stdout.write("\n[doctor --fix] generating Ed25519 keypair via `node lib/sign.js generate-keypair`...\n");
|
|
2505
|
+
const r = require("child_process").spawnSync(process.execPath, [path.join(PKG_ROOT, "lib", "sign.js"), "generate-keypair"], { stdio: "inherit", cwd: PKG_ROOT });
|
|
2506
|
+
if (r.status === 0) {
|
|
2507
|
+
process.stdout.write("[doctor --fix] keypair generated — re-run `exceptd doctor` to confirm.\n");
|
|
2508
|
+
} else {
|
|
2509
|
+
process.stdout.write(`[doctor --fix] generation failed (exit=${r.status}); run \`node lib/sign.js generate-keypair\` manually.\n`);
|
|
2510
|
+
process.exitCode = 1;
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
if (errorList.length > 0) process.exitCode = 1;
|
|
2515
|
+
// Warnings alone do NOT force exit 1 — CI gates use exit 0 to mean "ran
|
|
2516
|
+
// successfully" even with informational warnings. Operators reading the
|
|
2517
|
+
// visible "[!! warn]" line still see the issue.
|
|
2326
2518
|
}
|
|
2327
2519
|
|
|
2328
2520
|
function cmdListAttestations(runner, args, runOpts, pretty) {
|
|
@@ -2570,10 +2762,31 @@ function cmdAiRun(runner, args, runOpts, pretty) {
|
|
|
2570
2762
|
const tail = buf.trim();
|
|
2571
2763
|
if (tail) handleLine(tail);
|
|
2572
2764
|
if (!handled) {
|
|
2573
|
-
|
|
2765
|
+
// Bug #66 (v0.11.2): stdin closed without an evidence event. Before
|
|
2766
|
+
// declaring an error, try to interpret the raw stdin as a bare
|
|
2767
|
+
// submission object (the common shell-pipe case where `echo
|
|
2768
|
+
// '{...}' | exceptd ai-run secrets` pipes the submission body, not a
|
|
2769
|
+
// wrapped event). If it parses as such, run with it and complete the
|
|
2770
|
+
// phases. Otherwise emit the helpful error.
|
|
2771
|
+
const raw = (process.stdin._consumed || "") || buf;
|
|
2772
|
+
const allText = process.stdin._allText;
|
|
2773
|
+
if (allText && allText.trim()) {
|
|
2774
|
+
try {
|
|
2775
|
+
const parsed = JSON.parse(allText.trim());
|
|
2776
|
+
if (parsed && (parsed.observations || parsed.artifacts || parsed.signal_overrides || parsed.precondition_checks)) {
|
|
2777
|
+
handleLine(JSON.stringify({ event: "evidence", payload: parsed }));
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
} catch { /* fall through to error */ }
|
|
2781
|
+
}
|
|
2782
|
+
writeLine({ event: "error", reason: "stdin closed without an evidence event. Pipe `{\"event\":\"evidence\",\"payload\":{...}}` for streaming mode, or pass --no-stream + --evidence <file> for single-shot." });
|
|
2574
2783
|
process.exit(1);
|
|
2575
2784
|
}
|
|
2576
2785
|
});
|
|
2786
|
+
|
|
2787
|
+
// Capture stdin for the post-close fallback.
|
|
2788
|
+
process.stdin._allText = "";
|
|
2789
|
+
process.stdin.on("data", chunk => { process.stdin._allText += chunk.toString(); });
|
|
2577
2790
|
}
|
|
2578
2791
|
|
|
2579
2792
|
/**
|
|
@@ -2609,6 +2822,31 @@ function buildSubmissionFromPayload(payload) {
|
|
|
2609
2822
|
* phases.direct.threat_context. Returns the top 5 matches with a confidence
|
|
2610
2823
|
* score (matched tokens / total tokens).
|
|
2611
2824
|
*/
|
|
2825
|
+
/**
|
|
2826
|
+
* `ask "<question>"` — plain-English routing to playbook(s).
|
|
2827
|
+
*
|
|
2828
|
+
* v0.11.2 rewrite (#58 / #67): the v0.11.0 implementation only indexed
|
|
2829
|
+
* domain.name + attack_class + first sentence of threat_context, with a
|
|
2830
|
+
* length>3 token filter that dropped short but-meaningful words like "PQC"
|
|
2831
|
+
* or "MCP". The richer index now includes:
|
|
2832
|
+
* - playbook id
|
|
2833
|
+
* - domain.name + domain.attack_class
|
|
2834
|
+
* - domain.attack_refs (T-numbers) + atlas_refs (AML-numbers)
|
|
2835
|
+
* - domain.cwe_refs + frameworks_in_scope
|
|
2836
|
+
* - phases.govern.theater_fingerprints[].claim
|
|
2837
|
+
* - phases.direct.threat_context (full, not first sentence)
|
|
2838
|
+
* - phases.direct.framework_lag_declaration
|
|
2839
|
+
* - skill_chain skill names
|
|
2840
|
+
* - phases.look.collection_scope.asset_scope
|
|
2841
|
+
*
|
|
2842
|
+
* Token filter dropped to length >= 2 (was > 3) so "PQC" / "MCP" / "CI"
|
|
2843
|
+
* tokens match. Synonym map handles common operator phrasings ("API
|
|
2844
|
+
* keys" → secrets, "supply chain" → sbom / library-author, etc).
|
|
2845
|
+
*
|
|
2846
|
+
* Threshold: top match must have score >= 1 (was > 0; same). When no
|
|
2847
|
+
* playbook scores >= 1, fall back to substring match on playbook ID
|
|
2848
|
+
* itself ("secrets" → secrets playbook).
|
|
2849
|
+
*/
|
|
2612
2850
|
function cmdAsk(runner, args, runOpts, pretty) {
|
|
2613
2851
|
const question = (args._ || []).join(" ").trim();
|
|
2614
2852
|
if (!question) {
|
|
@@ -2616,42 +2854,97 @@ function cmdAsk(runner, args, runOpts, pretty) {
|
|
|
2616
2854
|
}
|
|
2617
2855
|
const ids = runner.listPlaybooks();
|
|
2618
2856
|
const q = question.toLowerCase();
|
|
2619
|
-
|
|
2857
|
+
|
|
2858
|
+
// Synonym expansion — common operator phrasings → playbook-relevant tokens.
|
|
2859
|
+
// Keeps cmdAsk dependency-free; rich enough to cover the 80% of natural
|
|
2860
|
+
// queries listed in the operator report.
|
|
2861
|
+
const SYNONYMS = {
|
|
2862
|
+
"credential": ["secret", "key", "token", "password", "cred"],
|
|
2863
|
+
"credentials": ["secret", "key", "token", "password", "cred"],
|
|
2864
|
+
"api key": ["secret", "credential"],
|
|
2865
|
+
"api keys": ["secret", "credential"],
|
|
2866
|
+
"supply chain": ["sbom", "dependency", "vendor", "package", "library", "publish"],
|
|
2867
|
+
"supply-chain": ["sbom", "dependency", "vendor", "package", "library", "publish"],
|
|
2868
|
+
"npm package": ["sbom", "dependency", "library", "publish"],
|
|
2869
|
+
"npm packages": ["sbom", "dependency", "library", "publish"],
|
|
2870
|
+
"pqc": ["post-quantum", "quantum", "crypto", "ml-kem", "ml-dsa", "kyber", "dilithium"],
|
|
2871
|
+
"quantum": ["pqc", "post-quantum"],
|
|
2872
|
+
"audit": ["scan", "review", "check", "validate", "verify"],
|
|
2873
|
+
"mcp": ["model context protocol", "tool", "ai-tool"],
|
|
2874
|
+
"ai": ["llm", "model", "anthropic", "openai", "claude"],
|
|
2875
|
+
"compliance": ["framework", "audit", "soc", "iso", "nist", "gdpr", "dora", "nis2", "regulator"],
|
|
2876
|
+
"kernel": ["lpe", "linux", "privilege", "escalation", "cve", "uname"],
|
|
2877
|
+
"container": ["docker", "kubernetes", "k8s", "compose", "image"],
|
|
2878
|
+
"secret": ["credential", "key", "token", "env", "leak"],
|
|
2879
|
+
"secrets": ["credential", "key", "token", "env", "leak", "repo"],
|
|
2880
|
+
"config": ["configuration", "settings"],
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
// Tokenize question (length >= 2, lowercase) + expand via synonyms.
|
|
2884
|
+
const baseTokens = q.split(/\W+/).filter(t => t.length >= 2);
|
|
2885
|
+
const expanded = new Set(baseTokens);
|
|
2886
|
+
// multi-word synonym keys
|
|
2887
|
+
for (const [phrase, syns] of Object.entries(SYNONYMS)) {
|
|
2888
|
+
if (q.includes(phrase)) for (const s of syns) expanded.add(s);
|
|
2889
|
+
}
|
|
2890
|
+
// single-word synonym keys
|
|
2891
|
+
for (const t of baseTokens) {
|
|
2892
|
+
if (SYNONYMS[t]) for (const s of SYNONYMS[t]) expanded.add(s);
|
|
2893
|
+
}
|
|
2894
|
+
const tokens = [...expanded];
|
|
2895
|
+
|
|
2620
2896
|
const scored = [];
|
|
2621
2897
|
for (const id of ids) {
|
|
2622
2898
|
let pb;
|
|
2623
2899
|
try { pb = runner.loadPlaybook(id); } catch { continue; }
|
|
2624
|
-
const threat = pb.phases?.direct?.threat_context || "";
|
|
2625
|
-
const firstSentence = threat.split(/(?<=[.!?])\s+/)[0] || "";
|
|
2626
2900
|
const haystack = [
|
|
2901
|
+
pb._meta?.id || id,
|
|
2627
2902
|
pb.domain?.name || "",
|
|
2628
2903
|
pb.domain?.attack_class || "",
|
|
2629
|
-
|
|
2904
|
+
...(pb.domain?.attack_refs || []),
|
|
2905
|
+
...(pb.domain?.atlas_refs || []),
|
|
2906
|
+
...(pb.domain?.cwe_refs || []),
|
|
2907
|
+
...(pb.domain?.frameworks_in_scope || []),
|
|
2908
|
+
...((pb.phases?.govern?.theater_fingerprints || []).map(t => t.claim || "")),
|
|
2909
|
+
...((pb.phases?.govern?.theater_fingerprints || []).map(t => t.pattern_id || "")),
|
|
2910
|
+
pb.phases?.direct?.threat_context || "",
|
|
2911
|
+
pb.phases?.direct?.framework_lag_declaration || "",
|
|
2912
|
+
...((pb.phases?.direct?.skill_chain || []).map(s => s.skill || "")),
|
|
2913
|
+
pb.phases?.look?.collection_scope?.asset_scope || "",
|
|
2914
|
+
pb.phases?.look?.collection_scope?.time_window || "",
|
|
2630
2915
|
].join(" ").toLowerCase();
|
|
2631
|
-
|
|
2916
|
+
let score = 0;
|
|
2917
|
+
for (const t of tokens) if (haystack.includes(t)) score++;
|
|
2918
|
+
// ID match counts double — "secrets" should map to the secrets playbook.
|
|
2919
|
+
if (tokens.some(t => (pb._meta?.id || id) === t)) score += 3;
|
|
2632
2920
|
scored.push({ id: pb._meta?.id || id, score });
|
|
2633
2921
|
}
|
|
2634
2922
|
scored.sort((a, b) => b.score - a.score);
|
|
2635
2923
|
const top = scored.filter(s => s.score > 0).slice(0, 5);
|
|
2636
2924
|
|
|
2925
|
+
// v0.11.2: default human-readable; --json for machine.
|
|
2637
2926
|
if (top.length === 0) {
|
|
2638
|
-
|
|
2927
|
+
const result = {
|
|
2639
2928
|
verb: "ask",
|
|
2640
2929
|
question,
|
|
2641
|
-
|
|
2930
|
+
routed_to: [],
|
|
2642
2931
|
hint: "No playbook matched. Try `exceptd brief --all` to see what's available, or `exceptd discover` to detect what's in your cwd.",
|
|
2643
|
-
}
|
|
2932
|
+
};
|
|
2933
|
+
if (args.json) return emit(result, pretty);
|
|
2934
|
+
process.stdout.write(`ask: ${question}\n no playbook matched.\n try: exceptd discover (auto-detect what's in your cwd)\n`);
|
|
2644
2935
|
return;
|
|
2645
2936
|
}
|
|
2646
2937
|
|
|
2647
|
-
|
|
2938
|
+
const result = {
|
|
2648
2939
|
verb: "ask",
|
|
2649
2940
|
question,
|
|
2650
2941
|
routed_to: top.map(t => t.id),
|
|
2651
|
-
confidence: top[0].score / Math.max(
|
|
2942
|
+
confidence: Math.min(1, top[0].score / Math.max(2, tokens.length)),
|
|
2652
2943
|
next_step: `exceptd run ${top[0].id} # or: exceptd brief ${top[0].id} to learn first`,
|
|
2653
2944
|
full_match_list: top,
|
|
2654
|
-
}
|
|
2945
|
+
};
|
|
2946
|
+
if (args.json) return emit(result, pretty);
|
|
2947
|
+
process.stdout.write(`ask: ${question}\n top match: ${top[0].id} (score ${top[0].score})\n next: ${result.next_step}\n alternates: ${top.slice(1).map(t => t.id).join(", ") || "(none)"}\n`);
|
|
2655
2948
|
}
|
|
2656
2949
|
|
|
2657
2950
|
/**
|
|
@@ -2668,11 +2961,29 @@ function cmdCi(runner, args, runOpts, pretty) {
|
|
|
2668
2961
|
const maxRwep = args["max-rwep"] !== undefined ? Number(args["max-rwep"]) : null;
|
|
2669
2962
|
const blockOnClock = !!args["block-on-jurisdiction-clock"];
|
|
2670
2963
|
|
|
2964
|
+
// v0.11.2 bug #63: align with discover. ci now includes cross-cutting
|
|
2965
|
+
// playbooks (framework) automatically, and when --scope code is set on a
|
|
2966
|
+
// cwd that has lockfiles, also includes sbom (which is scope:system but
|
|
2967
|
+
// applies to any repo). Operators expect ci to run "what discover would
|
|
2968
|
+
// recommend"; pre-0.11.2 ci ran scope=X only, which dropped framework +
|
|
2969
|
+
// missed sbom-on-repo.
|
|
2671
2970
|
let ids;
|
|
2672
2971
|
if (args.all) {
|
|
2673
2972
|
ids = runner.listPlaybooks();
|
|
2674
2973
|
} else if (scope) {
|
|
2675
2974
|
ids = filterPlaybooksByScope(runner, scope);
|
|
2975
|
+
// Always include cross-cutting playbooks regardless of scope choice.
|
|
2976
|
+
const cross = filterPlaybooksByScope(runner, "cross-cutting");
|
|
2977
|
+
ids = [...new Set([...ids, ...cross])];
|
|
2978
|
+
// For code-scope on a repo: also include sbom (system-scope but
|
|
2979
|
+
// repo-relevant) so ci output matches discover.
|
|
2980
|
+
if (scope === "code" && fs.existsSync(path.join(process.cwd(), ".git"))) {
|
|
2981
|
+
const hasLockfile = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "requirements.txt", "Pipfile.lock", "Cargo.lock", "go.sum"]
|
|
2982
|
+
.some(f => fs.existsSync(path.join(process.cwd(), f)));
|
|
2983
|
+
if (hasLockfile && runner.listPlaybooks().includes("sbom") && !ids.includes("sbom")) {
|
|
2984
|
+
ids.push("sbom");
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2676
2987
|
} else {
|
|
2677
2988
|
const scopes = detectScopes();
|
|
2678
2989
|
ids = scopes.flatMap(s => filterPlaybooksByScope(runner, s));
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-12T15:
|
|
3
|
+
"generated_at": "2026-05-12T15:46:10.198Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 49,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "8097ba32340aba872d73dd047bebb5f8d561cda8c55bcd224c67475c84cfac4b",
|
|
8
8
|
"data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
|
|
9
9
|
"data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
|
|
10
10
|
"data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
|
package/lib/playbook-runner.js
CHANGED
|
@@ -361,7 +361,17 @@ function detect(playbookId, directiveId, agentSubmission = {}) {
|
|
|
361
361
|
indicators: indicatorResults,
|
|
362
362
|
false_positive_checks_required: fpChecksRequired,
|
|
363
363
|
classification,
|
|
364
|
-
minimum_signal_basis: det.minimum_signal?.[classification === 'detected' ? 'detected' : classification === 'not_detected' ? 'not_detected' : 'inconclusive']
|
|
364
|
+
minimum_signal_basis: det.minimum_signal?.[classification === 'detected' ? 'detected' : classification === 'not_detected' ? 'not_detected' : 'inconclusive'],
|
|
365
|
+
// v0.11.3 #71: surface what detect actually consumed. Operators reading
|
|
366
|
+
// the detect output now see whether their flat-shape observations + the
|
|
367
|
+
// signal_overrides + the classification override all reached the runner.
|
|
368
|
+
// Pre-0.11.3 detect's output was opaque — "inconclusive" with no
|
|
369
|
+
// explanation gave operators no signal about what went wrong.
|
|
370
|
+
observations_received: Object.keys(agentSubmission.artifacts || {}),
|
|
371
|
+
signals_received: Object.keys(agentSubmission.signal_overrides || {}),
|
|
372
|
+
indicators_evaluated: indicatorResults.length,
|
|
373
|
+
classification_override_applied: validOverrides.has(override) ? (override === 'clean' ? 'not_detected' : override) : null,
|
|
374
|
+
submission_shape_seen: agentSubmission._original_shape || (agentSubmission.artifacts ? 'nested (v0.10.x)' : 'empty')
|
|
365
375
|
};
|
|
366
376
|
}
|
|
367
377
|
|
|
@@ -884,10 +894,29 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
|
|
|
884
894
|
*/
|
|
885
895
|
function normalizeSubmission(submission, playbook) {
|
|
886
896
|
if (!submission || typeof submission !== "object") return submission || {};
|
|
887
|
-
if (submission.artifacts || submission.signal_overrides || submission.signals) return submission;
|
|
888
|
-
if (!submission.observations && !submission.verdict) return submission;
|
|
889
897
|
|
|
890
|
-
|
|
898
|
+
// v0.11.3 #71 fix: the CLI may inject `signals._bundle_formats` before
|
|
899
|
+
// calling normalize (for --format <fmt> support). Pre-0.11.3 normalize
|
|
900
|
+
// detected the injected `signals` key and bailed, leaving the flat
|
|
901
|
+
// `observations` / `verdict` untranslated and breaking detect. The shape
|
|
902
|
+
// detector now treats `observations` or `verdict` as authoritative for
|
|
903
|
+
// "this is flat" — even when nested keys also exist — and merges any
|
|
904
|
+
// pre-existing nested keys into the normalized result.
|
|
905
|
+
const hasFlat = submission.observations || submission.verdict;
|
|
906
|
+
|
|
907
|
+
if (!hasFlat) {
|
|
908
|
+
// Truly already-nested. Mark shape and return.
|
|
909
|
+
if (!submission._original_shape) submission._original_shape = 'nested (v0.10.x)';
|
|
910
|
+
return submission;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const out = {
|
|
914
|
+
artifacts: { ...(submission.artifacts || {}) },
|
|
915
|
+
signal_overrides: { ...(submission.signal_overrides || {}) },
|
|
916
|
+
signals: { ...(submission.signals || {}) },
|
|
917
|
+
precondition_checks: { ...(submission.precondition_checks || {}) },
|
|
918
|
+
_original_shape: 'flat (v0.11.0)',
|
|
919
|
+
};
|
|
891
920
|
const knownPreconditions = new Set((playbook?._meta?.preconditions || []).map(p => p.id));
|
|
892
921
|
const knownArtifacts = new Set((playbook?.phases?.look?.artifacts || []).map(a => a.id));
|
|
893
922
|
|
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-12T15:10
|
|
3
|
+
"_generated_at": "2026-05-12T15:45:10.483Z",
|
|
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.3",
|
|
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:10
|
|
55
|
+
"signed_at": "2026-05-12T15:45:10.018Z",
|
|
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:10
|
|
119
|
+
"signed_at": "2026-05-12T15:45:10.020Z",
|
|
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:10
|
|
182
|
+
"signed_at": "2026-05-12T15:45:10.020Z",
|
|
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:10
|
|
228
|
+
"signed_at": "2026-05-12T15:45:10.021Z"
|
|
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:10
|
|
259
|
+
"signed_at": "2026-05-12T15:45:10.021Z"
|
|
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:10
|
|
288
|
+
"signed_at": "2026-05-12T15:45:10.022Z"
|
|
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:10
|
|
325
|
+
"signed_at": "2026-05-12T15:45:10.022Z",
|
|
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:10
|
|
382
|
+
"signed_at": "2026-05-12T15:45:10.022Z",
|
|
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:10
|
|
417
|
+
"signed_at": "2026-05-12T15:45:10.023Z",
|
|
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:10
|
|
445
|
+
"signed_at": "2026-05-12T15:45:10.023Z"
|
|
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:10
|
|
477
|
+
"signed_at": "2026-05-12T15:45:10.023Z"
|
|
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:10
|
|
504
|
+
"signed_at": "2026-05-12T15:45:10.024Z"
|
|
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:10
|
|
556
|
+
"signed_at": "2026-05-12T15:45:10.024Z",
|
|
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:10
|
|
603
|
+
"signed_at": "2026-05-12T15:45:10.025Z"
|
|
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:10
|
|
640
|
+
"signed_at": "2026-05-12T15:45:10.025Z",
|
|
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:10
|
|
675
|
+
"signed_at": "2026-05-12T15:45:10.025Z"
|
|
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:10
|
|
746
|
+
"signed_at": "2026-05-12T15:45:10.025Z"
|
|
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:10
|
|
806
|
+
"signed_at": "2026-05-12T15:45:10.026Z"
|
|
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:10
|
|
881
|
+
"signed_at": "2026-05-12T15:45:10.026Z"
|
|
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:10
|
|
958
|
+
"signed_at": "2026-05-12T15:45:10.026Z"
|
|
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:10
|
|
1015
|
+
"signed_at": "2026-05-12T15:45:10.027Z"
|
|
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:10
|
|
1082
|
+
"signed_at": "2026-05-12T15:45:10.027Z"
|
|
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:10
|
|
1138
|
+
"signed_at": "2026-05-12T15:45:10.027Z"
|
|
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:10
|
|
1190
|
+
"signed_at": "2026-05-12T15:45:10.027Z"
|
|
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:10
|
|
1240
|
+
"signed_at": "2026-05-12T15:45:10.028Z"
|
|
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:10
|
|
1314
|
+
"signed_at": "2026-05-12T15:45:10.028Z"
|
|
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:10
|
|
1364
|
+
"signed_at": "2026-05-12T15:45:10.028Z"
|
|
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:10
|
|
1424
|
+
"signed_at": "2026-05-12T15:45:10.029Z"
|
|
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:10
|
|
1505
|
+
"signed_at": "2026-05-12T15:45:10.029Z"
|
|
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:10
|
|
1574
|
+
"signed_at": "2026-05-12T15:45:10.029Z"
|
|
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:10
|
|
1639
|
+
"signed_at": "2026-05-12T15:45:10.030Z"
|
|
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:10
|
|
1708
|
+
"signed_at": "2026-05-12T15:45:10.030Z"
|
|
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:10
|
|
1789
|
+
"signed_at": "2026-05-12T15:45:10.030Z"
|
|
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:10
|
|
1851
|
+
"signed_at": "2026-05-12T15:45:10.031Z"
|
|
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:10
|
|
1922
|
+
"signed_at": "2026-05-12T15:45:10.031Z"
|
|
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:10
|
|
1984
|
+
"signed_at": "2026-05-12T15:45:10.031Z"
|
|
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:10
|
|
2037
|
+
"signed_at": "2026-05-12T15:45:10.032Z"
|
|
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:10
|
|
2105
|
+
"signed_at": "2026-05-12T15:45:10.032Z"
|
|
2106
2106
|
}
|
|
2107
2107
|
]
|
|
2108
2108
|
}
|
package/orchestrator/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/exceptd-skills",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
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:
|
|
4
|
+
"serialNumber": "urn:uuid:bc8a18f6-0f9a-4701-9673-d234acd23435",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-12T15:
|
|
7
|
+
"timestamp": "2026-05-12T15:45:10.912Z",
|
|
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.3",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.11.
|
|
19
|
+
"version": "0.11.3",
|
|
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.3",
|
|
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.3"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"type": "vcs",
|