@blamejs/exceptd-skills 0.15.45 → 0.15.46
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 +12 -0
- package/CONTEXT.md +1 -1
- package/bin/exceptd.js +59 -111
- package/data/_indexes/_meta.json +2 -2
- package/lib/flag-suggest.js +5 -9
- package/lib/lint-skills.js +18 -19
- package/lib/playbook-runner.js +11 -3
- package/lib/validate-catalog-meta.js +6 -5
- package/lib/validate-cve-catalog.js +8 -8
- package/lib/validate-playbooks.js +14 -14
- package/manifest.json +44 -44
- package/orchestrator/index.js +5 -1
- package/orchestrator/scanner.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +34 -34
- package/scripts/check-test-count.js +11 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.46 — 2026-05-30
|
|
4
|
+
|
|
5
|
+
A correctness and help-accuracy pass.
|
|
6
|
+
|
|
7
|
+
`exceptd help <verb>` for a verb removed in v0.13.0 — `plan`, `govern`, `direct`, `look`, `ingest` — now refuses with the replacement command and a non-zero exit, matching what the bare verb already did, instead of printing stale help for a command that no longer runs.
|
|
8
|
+
|
|
9
|
+
Help text now matches behavior: `brief --help` documents `--flat`, `attest --help` lists the `prune` subverb, and `ai-run --help` shows the correct exit code for a session-id collision. `doctor` accepts `--air-gap` consistently across its flag-validation paths. Error messages and accepted-verb lists no longer recommend removed verbs.
|
|
10
|
+
|
|
11
|
+
A failure opening the `watch --log-file` target now exits with the generic-failure code instead of the detected-escalation code, so a filesystem error no longer trips a CI gate keyed on escalation. The worst-of active-exploitation reduction used in finding drafts now ranks a "theoretical" CVE correctly instead of dropping it or overstating an empty set as "unknown".
|
|
12
|
+
|
|
13
|
+
Validator warnings describe their `--strict` / predeploy enforcement rather than promising an already-shipped version.
|
|
14
|
+
|
|
3
15
|
## 0.15.45 — 2026-05-30
|
|
4
16
|
|
|
5
17
|
An operator-experience pass.
|
package/CONTEXT.md
CHANGED
|
@@ -228,7 +228,7 @@ The `researcher` **skill** (front-door dispatcher) and `threat-researcher` **age
|
|
|
228
228
|
- `data/global-frameworks.json` — load for multi-jurisdiction questions
|
|
229
229
|
- `data/atlas-ttps.json`, `data/attack-techniques.json` — load for TTP-driven work
|
|
230
230
|
- Individual skill files — 15–40 KB each; load on match, not preemptively
|
|
231
|
-
- Playbook JSON — load on demand via `exceptd
|
|
231
|
+
- Playbook JSON — load on demand via `exceptd brief <playbook>` (or `--phase look` for just the artifact spec); the engine handles phase orchestration
|
|
232
232
|
|
|
233
233
|
### What This Repo Does Not Contain
|
|
234
234
|
|
package/bin/exceptd.js
CHANGED
|
@@ -28,13 +28,10 @@
|
|
|
28
28
|
* Seven-phase playbook contract (govern → direct → look → detect →
|
|
29
29
|
* analyze → validate → close):
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* direct <playbook> Phase 2: scope the investigation.
|
|
34
|
-
* look <playbook> Phase 3: emit artifact-collection spec for agent.
|
|
31
|
+
* brief [--all] Phases 1-3 (govern/direct/look) in one info doc.
|
|
32
|
+
* <playbook> Add --phase govern|direct|look for a single phase.
|
|
35
33
|
* run <playbook> Phases 4-7 (detect/analyze/validate/close) from
|
|
36
34
|
* agent submission JSON.
|
|
37
|
-
* ingest Alias for `run` matching AGENTS.md terminology.
|
|
38
35
|
* reattest <session> Re-run a prior session and diff evidence_hash.
|
|
39
36
|
*
|
|
40
37
|
* help, --help, -h This help.
|
|
@@ -169,7 +166,7 @@ const ORCHESTRATOR_PASSTHROUGH = new Set([
|
|
|
169
166
|
"framework-gap", "framework-gap-analysis",
|
|
170
167
|
]);
|
|
171
168
|
|
|
172
|
-
//
|
|
169
|
+
// Levenshtein-1 did-you-mean for unknown verbs.
|
|
173
170
|
// Catches common single-char / transposition typos against the COMMANDS
|
|
174
171
|
// table without false-positive flood: only suggests verbs within distance
|
|
175
172
|
// 1 (one insert / delete / substitute / transpose). For typed-distance 2+
|
|
@@ -385,6 +382,7 @@ Canonical verbs
|
|
|
385
382
|
--all every playbook
|
|
386
383
|
--scope <type> system | code | service | cross-cutting
|
|
387
384
|
--directives expand directive metadata
|
|
385
|
+
--flat ungrouped list (omit scope grouping)
|
|
388
386
|
--phase <name> emit only one phase (legacy compat)
|
|
389
387
|
|
|
390
388
|
run [playbook] Phases 4-7. Auto-detects cwd context when no
|
|
@@ -433,7 +431,7 @@ Canonical verbs
|
|
|
433
431
|
2 detected/escalate, 3 ran-but-no-evidence,
|
|
434
432
|
4 blocked (ok:false), 5 jurisdiction clock started.
|
|
435
433
|
(Codes 6/7/8/9 surface on attest verify / run /
|
|
436
|
-
ai-run
|
|
434
|
+
ai-run, not ci.)
|
|
437
435
|
--all | --scope <type> | (auto-detect)
|
|
438
436
|
--max-rwep <n> cap below playbook default
|
|
439
437
|
--block-on-jurisdiction-clock
|
|
@@ -597,13 +595,25 @@ function main() {
|
|
|
597
595
|
const rest = argv.slice(1);
|
|
598
596
|
|
|
599
597
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
600
|
-
//
|
|
598
|
+
// `exceptd help <verb>` previously dropped the
|
|
601
599
|
// verb argument and printed the top-level help. Route through the same
|
|
602
600
|
// printPlaybookVerbHelp() that `exceptd <verb> --help` already uses so
|
|
603
601
|
// operators get a consistent verb-specific help surface regardless of
|
|
604
602
|
// which way they reached it.
|
|
605
603
|
if (rest.length > 0 && typeof rest[0] === 'string' && rest[0].length > 0) {
|
|
606
604
|
const verb = rest[0];
|
|
605
|
+
// A removed verb has no live help. Refuse with the same structured
|
|
606
|
+
// removal error the bare verb emits, so `help <removed>` and
|
|
607
|
+
// `<removed> --help` agree (both exit non-zero, both name the
|
|
608
|
+
// replacement) instead of printing stale help for a verb that no
|
|
609
|
+
// longer dispatches.
|
|
610
|
+
if (REMOVED_VERBS[verb]) {
|
|
611
|
+
emitError(
|
|
612
|
+
`'${verb}' was removed in v0.13.0. Use \`exceptd ${REMOVED_VERBS[verb]}\` instead.`,
|
|
613
|
+
{ verb, removed_in: "0.13.0", replacement: REMOVED_VERBS[verb] }
|
|
614
|
+
);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
607
617
|
if (printPlaybookVerbHelp(verb)) {
|
|
608
618
|
process.exit(0);
|
|
609
619
|
}
|
|
@@ -730,7 +740,7 @@ function main() {
|
|
|
730
740
|
// UNKNOWN_COMMAND (10) afterwards. Cycle 9 split this away from
|
|
731
741
|
// DETECTED_ESCALATE (2) — the two semantics had collided since v0.12.24.
|
|
732
742
|
//
|
|
733
|
-
//
|
|
743
|
+
// add a did-you-mean suggestion when the
|
|
734
744
|
// unknown verb is within Levenshtein-1 of a real verb (catches the
|
|
735
745
|
// common single-char typos: `discoer` → `discover`, `attst` → `attest`,
|
|
736
746
|
// `valdiate-cves` → `validate-cves`).
|
|
@@ -1034,7 +1044,7 @@ function readEvidence(evidenceFlag, opts = {}) {
|
|
|
1034
1044
|
}
|
|
1035
1045
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
1036
1046
|
if (!text.trim()) {
|
|
1037
|
-
//
|
|
1047
|
+
// pre-fix empty stdin silently became {}
|
|
1038
1048
|
// — operator got a "successful" run on no evidence with no warning,
|
|
1039
1049
|
// and the evidence_hash for `{}` is deterministic so subsequent
|
|
1040
1050
|
// runs didn't even reveal the mistake. Emit a stderr nudge so the
|
|
@@ -1138,7 +1148,7 @@ function hasReadableStdin() {
|
|
|
1138
1148
|
// PowerShell / MSYS pipes working (isTTY === false when piped). Do NOT
|
|
1139
1149
|
// gate on size > 0 here: a Windows pipe with bytes queued reports as
|
|
1140
1150
|
// a regular file with size 0, and gating would silently skip every
|
|
1141
|
-
// `echo {...} | exceptd run|
|
|
1151
|
+
// `echo {...} | exceptd run|ai-run` invocation.
|
|
1142
1152
|
if (process.platform === "win32" && process.stdin.isTTY === false) return true;
|
|
1143
1153
|
return false;
|
|
1144
1154
|
}
|
|
@@ -1561,16 +1571,15 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
1561
1571
|
}
|
|
1562
1572
|
|
|
1563
1573
|
// --csaf-status and --publisher-namespace shape the CSAF bundle emitted by
|
|
1564
|
-
// phases 5-7. Verbs that don't drive those phases (brief,
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
// to run.
|
|
1574
|
+
// phases 5-7. Verbs that don't drive those phases (brief, attest,
|
|
1575
|
+
// list-attestations, discover, doctor, lint, ask, verify-attestation,
|
|
1576
|
+
// reattest) never assemble a bundle, so silently consuming these flags is
|
|
1577
|
+
// a UX trap. Refuse on those verbs so the operator knows the flag was
|
|
1578
|
+
// discarded — same pattern as --ack. Error message templates and emitError
|
|
1579
|
+
// prefixes use the in-scope `cmd` verb so a brief invocation says "brief:"
|
|
1580
|
+
// rather than misattributing the flag to run.
|
|
1572
1581
|
const BUNDLE_FLAG_RELEVANT_VERBS = new Set([
|
|
1573
|
-
"run", "ci", "run-all", "ai-run",
|
|
1582
|
+
"run", "ci", "run-all", "ai-run",
|
|
1574
1583
|
]);
|
|
1575
1584
|
|
|
1576
1585
|
// --publisher-namespace <url> threads into the CSAF
|
|
@@ -1722,15 +1731,14 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
1722
1731
|
// consent was explicit vs. implicit. AGENTS.md says the AI should surface
|
|
1723
1732
|
// and wait for ack — this is how the ack gets recorded.
|
|
1724
1733
|
//
|
|
1725
|
-
// --ack only makes sense on verbs that drive phases 5-7 (run /
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1731
|
-
// is irrelevant.
|
|
1734
|
+
// --ack only makes sense on verbs that drive phases 5-7 (run / ai-run /
|
|
1735
|
+
// ci / run-all / reattest). Info-only verbs (brief, attest,
|
|
1736
|
+
// list-attestations, discover, doctor, lint, ask, verify-attestation)
|
|
1737
|
+
// never consume an attestation clock — accepting --ack silently is a UX
|
|
1738
|
+
// trap where operators believe they have recorded consent. Refuse on those
|
|
1739
|
+
// verbs so the operator knows the flag is irrelevant.
|
|
1732
1740
|
const ACK_RELEVANT_VERBS = new Set([
|
|
1733
|
-
"run", "
|
|
1741
|
+
"run", "ai-run", "ci", "run-all", "reattest",
|
|
1734
1742
|
]);
|
|
1735
1743
|
if (args.ack) {
|
|
1736
1744
|
if (!ACK_RELEVANT_VERBS.has(cmd)) {
|
|
@@ -1894,44 +1902,6 @@ With <id>: expands that recipe's ordered skill_chain and notes.
|
|
|
1894
1902
|
|
|
1895
1903
|
Flags:
|
|
1896
1904
|
--json Machine-readable output.`,
|
|
1897
|
-
plan: `plan — list playbooks + directives, grouped by scope.
|
|
1898
|
-
|
|
1899
|
-
Flags:
|
|
1900
|
-
--playbook <id> ... Filter to one or more playbook IDs.
|
|
1901
|
-
--scope <type> Filter by scope: system | code | service | cross-cutting | all
|
|
1902
|
-
--flat Disable grouped-by-scope output; emit flat list.
|
|
1903
|
-
--directives Include directive id + title + applies_to per playbook.
|
|
1904
|
-
--session-id <id> Reuse a specific session ID for the planning output.
|
|
1905
|
-
--mode <m> Investigation mode forwarded into govern.
|
|
1906
|
-
--pretty Indented JSON output.`,
|
|
1907
|
-
govern: `govern <playbook> — phase 1, load GRC context for a playbook.
|
|
1908
|
-
|
|
1909
|
-
Args / flags:
|
|
1910
|
-
<playbook> Playbook ID. Required positional.
|
|
1911
|
-
--directive <id> Specific directive (default: first one).
|
|
1912
|
-
--mode <m> Investigation mode forwarded into govern policy.
|
|
1913
|
-
--air-gap Honor _meta.air_gap_mode + air_gap_alternative paths.
|
|
1914
|
-
--pretty Indented JSON output.
|
|
1915
|
-
|
|
1916
|
-
Output: jurisdiction_obligations, theater_fingerprints, framework_context, skill_preload.`,
|
|
1917
|
-
direct: `direct <playbook> — phase 2, threat context + skill chain + token budget.
|
|
1918
|
-
|
|
1919
|
-
Args / flags:
|
|
1920
|
-
<playbook> Required positional.
|
|
1921
|
-
--directive <id> Specific directive (default: first one).
|
|
1922
|
-
--pretty Indented JSON output.`,
|
|
1923
|
-
look: `look <playbook> — phase 3, artifact-collection spec the host AI executes.
|
|
1924
|
-
|
|
1925
|
-
Args / flags:
|
|
1926
|
-
<playbook> Required positional.
|
|
1927
|
-
--directive <id> Specific directive (default: first one).
|
|
1928
|
-
--air-gap Honor air_gap_alternative paths.
|
|
1929
|
-
--pretty Indented JSON output.
|
|
1930
|
-
|
|
1931
|
-
Output includes a 'preconditions' array — the host AI MUST verify each
|
|
1932
|
-
precondition with its own probes and declare results back in the submission as:
|
|
1933
|
-
{ "precondition_checks": { "<id>": true | false } }
|
|
1934
|
-
The runner refuses the run if a precondition with on_fail=halt is unverified.`,
|
|
1935
1905
|
run: `run [playbook] — phases 4-7 (detect → analyze → validate → close).
|
|
1936
1906
|
|
|
1937
1907
|
Invocation modes:
|
|
@@ -2047,36 +2017,6 @@ Other operator-facing flags (full list in source; surfaced here for grep):
|
|
|
2047
2017
|
--attestation-root <p> Override .exceptd/ root for this run.
|
|
2048
2018
|
--mode <m> Investigation mode (self_service | authorized_pentest
|
|
2049
2019
|
| ir_response | ctf | research | compliance_audit).`,
|
|
2050
|
-
ingest: `ingest — alias for 'run' matching AGENTS.md terminology.
|
|
2051
|
-
|
|
2052
|
-
Flags:
|
|
2053
|
-
--domain <id> Playbook ID (overrides submission.playbook_id).
|
|
2054
|
-
--directive <id> Directive ID (overrides submission.directive_id).
|
|
2055
|
-
--evidence <file|-> Submission JSON. May include playbook_id/directive_id.
|
|
2056
|
-
--session-id <id> Reuse a specific session id (must satisfy
|
|
2057
|
-
/^[A-Za-z0-9._-]{1,64}$/).
|
|
2058
|
-
--force-overwrite Override session-id collision refusal.
|
|
2059
|
-
--operator <name> Bind attestation to a specific identity.
|
|
2060
|
-
--ack Explicit operator consent for jurisdiction clock.
|
|
2061
|
-
--attestation-root <p> Override .exceptd/ root for this ingest.
|
|
2062
|
-
--mode <m> Investigation mode (self_service | authorized_pentest
|
|
2063
|
-
| ir_response | ctf | research | compliance_audit).
|
|
2064
|
-
--air-gap Honor air_gap_alternative paths.
|
|
2065
|
-
--force-stale Override threat_currency_score<50 gate.
|
|
2066
|
-
--csaf-status <s> CSAF tracking.status for the close.evidence_package
|
|
2067
|
-
bundle. One of: draft | interim (default) | final.
|
|
2068
|
-
'final' commits to CSAF §3.1.11.3.5.1 immutability —
|
|
2069
|
-
set this only after operator review of the advisory.
|
|
2070
|
-
--publisher-namespace <url>
|
|
2071
|
-
CSAF document.publisher.namespace (§3.1.7.4). The
|
|
2072
|
-
operator's organisation URL, NOT the tooling vendor.
|
|
2073
|
-
Must be an http://… or https://… URL, ≤256 chars.
|
|
2074
|
-
--bundle-deterministic Emit byte-stable bundles (frozen timestamps).
|
|
2075
|
-
--bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.
|
|
2076
|
-
--pretty Indented JSON output.
|
|
2077
|
-
|
|
2078
|
-
Exit codes: 0 PASS, 1 framework, 4 blocked, 7 SESSION_ID_COLLISION,
|
|
2079
|
-
8 LOCK_CONTENTION, 9 STORAGE_EXHAUSTED.`,
|
|
2080
2020
|
reattest: `reattest [<session-id> | --latest] — replay a prior session and diff the evidence_hash.
|
|
2081
2021
|
|
|
2082
2022
|
Args / flags:
|
|
@@ -2102,7 +2042,7 @@ Lists every attestation under .exceptd/attestations/<session_id>/, sorted
|
|
|
2102
2042
|
newest-first, with truncated evidence_hash + capture timestamp + file path.`,
|
|
2103
2043
|
attest: `attest <subverb> <session-id> — auditor-facing attestation operations.
|
|
2104
2044
|
|
|
2105
|
-
Subverbs (list | show | export | verify | diff):
|
|
2045
|
+
Subverbs (list | show | export | verify | diff | prune):
|
|
2106
2046
|
attest show <sid> Emit the full (unredacted) attestation.
|
|
2107
2047
|
attest list Inventory every prior attestation under
|
|
2108
2048
|
~/.exceptd/attestations/ (or EXCEPTD_HOME when set).
|
|
@@ -2126,6 +2066,9 @@ Subverbs (list | show | export | verify | diff):
|
|
|
2126
2066
|
for the same playbook, or against --against <other-sid>
|
|
2127
2067
|
for an explicit pair. Reports unchanged | drifted |
|
|
2128
2068
|
resolved per evidence_hash + classification deltas.
|
|
2069
|
+
attest prune GC stale sessions: delete attestations older than
|
|
2070
|
+
--all-older-than <ISO>. --dry-run previews the set
|
|
2071
|
+
without deleting.
|
|
2129
2072
|
|
|
2130
2073
|
All subverbs honor --pretty for indented JSON output.
|
|
2131
2074
|
|
|
@@ -2241,7 +2184,7 @@ Flags:
|
|
|
2241
2184
|
Exit codes:
|
|
2242
2185
|
0 done Run completed; emitted {"event":"done","ok":true}.
|
|
2243
2186
|
1 framework error Engine threw or stdin parse failure.
|
|
2244
|
-
|
|
2187
|
+
7 SESSION_ID_COLLISION --session-id duplicate; pass --force-overwrite or fresh id.
|
|
2245
2188
|
8 LOCK_CONTENTION Concurrent persistAttestation lock held.
|
|
2246
2189
|
9 STORAGE_EXHAUSTED Disk/quota/RO filesystem on attestation write.
|
|
2247
2190
|
|
|
@@ -2352,7 +2295,7 @@ Exit codes:
|
|
|
2352
2295
|
etc.) and the operator has not acked.
|
|
2353
2296
|
|
|
2354
2297
|
(ci does not persist attestations per-run; exit codes 6/7/8/9 surface on
|
|
2355
|
-
\`attest verify\` and on \`run\` / \`ai-run
|
|
2298
|
+
\`attest verify\` and on \`run\` / \`ai-run\`, not on \`ci\`.)
|
|
2356
2299
|
|
|
2357
2300
|
Output: verb, session_id, playbooks_run, summary{total, detected,
|
|
2358
2301
|
max_rwep_observed, jurisdiction_clocks_started, verdict, fail_reasons[]},
|
|
@@ -2373,9 +2316,10 @@ Flags:
|
|
|
2373
2316
|
submission, not a human digest).`,
|
|
2374
2317
|
brief: `brief [playbook] — unified info doc (v0.11.0).
|
|
2375
2318
|
|
|
2376
|
-
Collapses the
|
|
2377
|
-
|
|
2378
|
-
informational; brief reads them in one
|
|
2319
|
+
Collapses the info-only phases govern + direct + look into a single document,
|
|
2320
|
+
and replaces the removed plan / govern / direct / look verbs. Phases 1-3 of
|
|
2321
|
+
the seven-phase contract are entirely informational; brief reads them in one
|
|
2322
|
+
CLI invocation instead of three.
|
|
2379
2323
|
|
|
2380
2324
|
Modes:
|
|
2381
2325
|
brief Auto-detect playbooks for the cwd. Returns a list.
|
|
@@ -2389,6 +2333,8 @@ Modes:
|
|
|
2389
2333
|
|
|
2390
2334
|
Flags:
|
|
2391
2335
|
--directives Expand directive metadata per playbook.
|
|
2336
|
+
--flat Ungrouped playbook list (omit grouped_by_scope +
|
|
2337
|
+
scope_summary). Use with --all / --scope.
|
|
2392
2338
|
--pretty Indented JSON output.
|
|
2393
2339
|
--json Force single-line JSON.
|
|
2394
2340
|
|
|
@@ -2431,7 +2377,7 @@ Flags (selected — see \`exceptd run --help\` for the full list):
|
|
|
2431
2377
|
--bundle-deterministic Emit byte-stable bundles across the multi-run set.
|
|
2432
2378
|
--bundle-epoch <ISO> Frozen epoch for --bundle-deterministic.`,
|
|
2433
2379
|
};
|
|
2434
|
-
//
|
|
2380
|
+
// return whether a verb-specific help block was
|
|
2435
2381
|
// found so the `exceptd help <verb>` caller can decide whether to fall
|
|
2436
2382
|
// through to the top-level help (verb unknown) or stop here (verb known).
|
|
2437
2383
|
if (cmds[verb]) {
|
|
@@ -2988,7 +2934,7 @@ function cmdPlan(runner, args, runOpts, pretty) {
|
|
|
2988
2934
|
}
|
|
2989
2935
|
}
|
|
2990
2936
|
emit(plan, pretty, (obj) => {
|
|
2991
|
-
// Human renderer for `brief` / `brief --all
|
|
2937
|
+
// Human renderer for `brief` / `brief --all`. Pre-fix this
|
|
2992
2938
|
// verb dumped 36+ KB of JSON to the terminal — operators running
|
|
2993
2939
|
// `exceptd brief` to explore had no scannable view.
|
|
2994
2940
|
const lines = [];
|
|
@@ -4380,9 +4326,11 @@ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
|
|
|
4380
4326
|
// the aggregate JSON emitted above is allowed to fully drain.
|
|
4381
4327
|
//
|
|
4382
4328
|
// Aggregate exit-code precedence: LOCK_CONTENTION > STORAGE_EXHAUSTED >
|
|
4383
|
-
//
|
|
4384
|
-
// storage exhaustion is an infra event
|
|
4385
|
-
//
|
|
4329
|
+
// SESSION_ID_COLLISION > GENERIC_FAILURE. Lock contention is transient
|
|
4330
|
+
// (retry-from-outside fixes it); storage exhaustion is an infra event
|
|
4331
|
+
// requiring operator action; a session-id collision mirrors the single-run
|
|
4332
|
+
// code; any remaining ok:false per-playbook result yields GENERIC_FAILURE
|
|
4333
|
+
// (exit 1) — distinct from the single-run BLOCKED (4) path. Surfacing the
|
|
4386
4334
|
// most-specific code first means a CI gate can branch on the right
|
|
4387
4335
|
// remediation without parsing the body.
|
|
4388
4336
|
const anyLockBusy = results.some(r => r.attestation_persist && r.attestation_persist.lock_contention === true);
|
|
@@ -7506,7 +7454,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
|
|
|
7506
7454
|
}
|
|
7507
7455
|
// Enumerate sessions across both v0.11.0 default root and legacy cwd-
|
|
7508
7456
|
// relative root, so operators with prior attestations still see them.
|
|
7509
|
-
//
|
|
7457
|
+
// also track candidate roots that didn't exist
|
|
7510
7458
|
// so operators can tell whether the directory was scanned-and-empty or
|
|
7511
7459
|
// simply never created. Pre-fix the human output said "(no attestations
|
|
7512
7460
|
// under )" with no path — operators couldn't see where the verb looked.
|
|
@@ -7593,7 +7541,7 @@ function cmdListAttestations(runner, args, runOpts, pretty) {
|
|
|
7593
7541
|
limit: limitN,
|
|
7594
7542
|
filter: { playbook: playbookFilter ? [...playbookFilter] : null, since: args.since || null },
|
|
7595
7543
|
roots_searched: [...seenRoots],
|
|
7596
|
-
//
|
|
7544
|
+
// every candidate root + whether it existed,
|
|
7597
7545
|
// so JSON consumers can distinguish scanned-and-empty from never-created.
|
|
7598
7546
|
// The human renderer below also surfaces this rather than printing
|
|
7599
7547
|
// "(no attestations under )" with an empty path list.
|
|
@@ -8395,7 +8343,7 @@ function cmdCi(runner, args, runOpts, pretty) {
|
|
|
8395
8343
|
// --scope and --all. Operators specifying an explicit set get exactly that
|
|
8396
8344
|
// set, no more, no less. Pre-0.11.9 the flag was silently ignored.
|
|
8397
8345
|
let ids;
|
|
8398
|
-
//
|
|
8346
|
+
// positional args (`exceptd ci kernel cred-stores`)
|
|
8399
8347
|
// were silently ignored and the cwd-autodetect path ran instead. Operators
|
|
8400
8348
|
// got a green PASS for playbooks that were never actually executed. Treat
|
|
8401
8349
|
// positional args as an inline --required, with the same unknown-id refusal.
|
|
@@ -8531,7 +8479,7 @@ function cmdCi(runner, args, runOpts, pretty) {
|
|
|
8531
8479
|
let clockStartedReasons = [];
|
|
8532
8480
|
|
|
8533
8481
|
for (const id of ids) {
|
|
8534
|
-
//
|
|
8482
|
+
// defense-in-depth — validate id even though the catalog-iter
|
|
8535
8483
|
// upstream is trusted. A corrupt catalog returning a malformed id would
|
|
8536
8484
|
// otherwise reach loadPlaybook unchecked. Matches the cmdRunMulti pattern.
|
|
8537
8485
|
const idCheck = validateIdComponent(id, "playbook");
|
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-30T18:00:00.612Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 54,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "cfe4088da8f1fdddb4218f88bbadce04004046ad7105c5e16cc58fdf1aa958b8",
|
|
8
8
|
"data/atlas-ttps.json": "878b4a08bb73c8d20396d85cf433a88f2bc5e7a8cbf7f6ab773ce7ede0a11251",
|
|
9
9
|
"data/attack-techniques.json": "84fad74c8497cab922ed64b814752f54aa4620c2a938cb06642ff1510e1c5cb3",
|
|
10
10
|
"data/cve-catalog.json": "7a5f4e31401505e53330cdc4b54b39f8a8b04459d6b9411676d291c583ae535f",
|
package/lib/flag-suggest.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* distance ≤ 2 AND ≤ floor(flag.length / 2).
|
|
11
11
|
*
|
|
12
12
|
* Per-verb allowlists are the canonical CLI surface. Adding a new flag to a
|
|
13
|
-
* verb means appending to the allowlist here AND updating the
|
|
14
|
-
* block;
|
|
13
|
+
* verb means appending to the allowlist here AND updating the
|
|
14
|
+
* printPlaybookVerbHelp block; keep the two in sync. doctor maintains its own
|
|
15
|
+
* KNOWN_DOCTOR_FLAGS set in bin/exceptd.js — keep VERB_FLAG_ALLOWLIST.doctor
|
|
16
|
+
* aligned with it (tests/lib-flag-suggest.test.js pins the shared flags).
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
function editDistance(a, b) {
|
|
@@ -96,12 +98,6 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
|
|
|
96
98
|
'mode', 'force-stale', 'tlp',
|
|
97
99
|
'bundle-deterministic', 'bundle-epoch',
|
|
98
100
|
],
|
|
99
|
-
ingest: [
|
|
100
|
-
'evidence', 'session-id', 'force-overwrite', 'attestation-root', 'operator',
|
|
101
|
-
'ack', 'csaf-status', 'publisher-namespace', 'air-gap', 'force-stale',
|
|
102
|
-
'strict-preconditions',
|
|
103
|
-
'bundle-deterministic', 'bundle-epoch',
|
|
104
|
-
],
|
|
105
101
|
brief: ['all', 'scope', 'directives', 'flat', 'phase'],
|
|
106
102
|
discover: ['scan-only', 'scope', 'cwd'],
|
|
107
103
|
ask: [],
|
|
@@ -112,7 +108,7 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
|
|
|
112
108
|
reattest: [
|
|
113
109
|
'playbook', 'since', 'latest', 'force-replay', 'attestation-root',
|
|
114
110
|
],
|
|
115
|
-
doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors'],
|
|
111
|
+
doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors', 'air-gap'],
|
|
116
112
|
lint: ['evidence'],
|
|
117
113
|
collect: ['cwd', 'attest-ownership', 'resolve'],
|
|
118
114
|
refresh: [
|
package/lib/lint-skills.js
CHANGED
|
@@ -91,14 +91,13 @@ const REQUIRED_SECTIONS = [
|
|
|
91
91
|
|
|
92
92
|
// L3 — Defensive Countermeasure Mapping became a required section for skills
|
|
93
93
|
// reviewed on or after this cutoff (documented in AGENTS.md). Pre-cutoff
|
|
94
|
-
// skills remain exempt to preserve patch-class compatibility
|
|
95
|
-
// broaden the cutoff.
|
|
94
|
+
// skills remain exempt to preserve patch-class compatibility.
|
|
96
95
|
const COUNTERMEASURE_SECTION = 'Defensive Countermeasure Mapping';
|
|
97
96
|
const COUNTERMEASURE_CUTOFF = '2026-05-11';
|
|
98
97
|
|
|
99
98
|
// L1 — Minimum number of words of body text between a section heading and the
|
|
100
99
|
// next heading (or EOF) for the section to count as populated. Header-only
|
|
101
|
-
// sections surface as
|
|
100
|
+
// sections surface as warnings; promoted to failures under --strict.
|
|
102
101
|
const MIN_SECTION_BODY_WORDS = 20;
|
|
103
102
|
|
|
104
103
|
const PLACEHOLDER_PATTERNS = [
|
|
@@ -396,8 +395,8 @@ function validateFrontmatter(fm, skillName) {
|
|
|
396
395
|
* in the body (case-insensitive). Hard failure.
|
|
397
396
|
* - headerOnly[] — sections whose heading exists but whose body between
|
|
398
397
|
* that heading and the next heading is shorter than
|
|
399
|
-
* MIN_SECTION_BODY_WORDS words.
|
|
400
|
-
*
|
|
398
|
+
* MIN_SECTION_BODY_WORDS words. A warning by default;
|
|
399
|
+
* promoted to an error under --strict. */
|
|
401
400
|
function findMissingSections(body, requiredSections) {
|
|
402
401
|
const sections = requiredSections || REQUIRED_SECTIONS;
|
|
403
402
|
const lines = body.split(/\r?\n/);
|
|
@@ -563,16 +562,16 @@ function lintSkill(entry, ctx) {
|
|
|
563
562
|
}
|
|
564
563
|
}
|
|
565
564
|
|
|
566
|
-
// L2 — attack_refs cross-catalog resolution. Surface as
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
565
|
+
// L2 — attack_refs cross-catalog resolution. Surface as warnings by
|
|
566
|
+
// default (preserving patch-class compatibility); promoted to hard
|
|
567
|
+
// failures under --strict (the predeploy gate). If
|
|
568
|
+
// data/attack-techniques.json is missing entirely the ctx.attackKeys set
|
|
569
|
+
// is null — skip the check (the gate degrades gracefully).
|
|
571
570
|
if (Array.isArray(fm.attack_refs) && ctx.attackKeys) {
|
|
572
571
|
for (const ref of fm.attack_refs) {
|
|
573
572
|
if (!ctx.attackKeys.has(ref)) {
|
|
574
573
|
skillWarnings.push(
|
|
575
|
-
`attack_refs: "${ref}" not present in data/attack-techniques.json (
|
|
574
|
+
`attack_refs: "${ref}" not present in data/attack-techniques.json (an error under --strict)`,
|
|
576
575
|
);
|
|
577
576
|
}
|
|
578
577
|
}
|
|
@@ -617,18 +616,18 @@ function lintSkill(entry, ctx) {
|
|
|
617
616
|
|
|
618
617
|
// L3 — Defensive Countermeasure Mapping is required for skills reviewed
|
|
619
618
|
// on or after COUNTERMEASURE_CUTOFF. Pre-cutoff skills are exempt. The
|
|
620
|
-
// section's absence on a post-cutoff skill is a
|
|
621
|
-
// existing skills can add the section gradually;
|
|
622
|
-
//
|
|
619
|
+
// section's absence on a post-cutoff skill is a warning by default so
|
|
620
|
+
// existing skills can add the section gradually; promoted to a hard
|
|
621
|
+
// failure under --strict.
|
|
623
622
|
const { missing, headerOnly } = findMissingSections(body, REQUIRED_SECTIONS);
|
|
624
623
|
for (const s of missing) {
|
|
625
624
|
skillErrors.push(`body: missing required section "${s}"`);
|
|
626
625
|
}
|
|
627
626
|
for (const ho of headerOnly) {
|
|
628
|
-
// L1 — Header-only sections are
|
|
629
|
-
//
|
|
627
|
+
// L1 — Header-only sections are warnings by default; promoted to a
|
|
628
|
+
// failure under --strict.
|
|
630
629
|
skillWarnings.push(
|
|
631
|
-
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS});
|
|
630
|
+
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
|
|
632
631
|
);
|
|
633
632
|
}
|
|
634
633
|
if (
|
|
@@ -639,12 +638,12 @@ function lintSkill(entry, ctx) {
|
|
|
639
638
|
const cmResult = findMissingSections(body, [COUNTERMEASURE_SECTION]);
|
|
640
639
|
if (cmResult.missing.length > 0) {
|
|
641
640
|
skillWarnings.push(
|
|
642
|
-
`body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF};
|
|
641
|
+
`body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; an error under --strict)`,
|
|
643
642
|
);
|
|
644
643
|
} else {
|
|
645
644
|
for (const ho of cmResult.headerOnly) {
|
|
646
645
|
skillWarnings.push(
|
|
647
|
-
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS});
|
|
646
|
+
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
|
|
648
647
|
);
|
|
649
648
|
}
|
|
650
649
|
}
|
package/lib/playbook-runner.js
CHANGED
|
@@ -74,7 +74,7 @@ try {
|
|
|
74
74
|
|
|
75
75
|
// Probe the catalog (parse it, surface any load error) LAZILY on first need
|
|
76
76
|
// rather than at module load. The probe parses the ~2.6MB CVE catalog (~8.5ms);
|
|
77
|
-
// doing it eagerly charged that to every cheap verb (brief/
|
|
77
|
+
// doing it eagerly charged that to every cheap verb (brief/ask/lint/
|
|
78
78
|
// discover) that never analyzes. run() calls this before the analyze path, so
|
|
79
79
|
// a corrupt catalog still surfaces as blocked_by:'catalog_corrupt' before
|
|
80
80
|
// analyze — just not on verbs that don't touch the catalog. Memoized: probes
|
|
@@ -1829,7 +1829,11 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
|
|
|
1829
1829
|
// Severity ladder for active_exploitation. The worst-of reduction lets
|
|
1830
1830
|
// analyzeFindingShape report the most-exploited CVE in the matched set, not
|
|
1831
1831
|
// the first-encountered one. Higher index = worse.
|
|
1832
|
-
|
|
1832
|
+
// `theoretical` (PoC exists, no in-the-wild use) must rank between `none` and
|
|
1833
|
+
// `unknown`; omitting it made `?? -1` lose to the -1 start, so an all-theoretical
|
|
1834
|
+
// matched set wrongly reduced to 'unknown' and a theoretical+none set dropped
|
|
1835
|
+
// the theoretical entry entirely. This vocabulary is first-class in scoring.js.
|
|
1836
|
+
const ACTIVE_EXPLOITATION_RANK = { none: 0, theoretical: 1, unknown: 2, suspected: 3, confirmed: 4 };
|
|
1833
1837
|
|
|
1834
1838
|
function worstActiveExploitation(matchedCves) {
|
|
1835
1839
|
let worst = null;
|
|
@@ -1840,7 +1844,9 @@ function worstActiveExploitation(matchedCves) {
|
|
|
1840
1844
|
const rank = ACTIVE_EXPLOITATION_RANK[v] ?? -1;
|
|
1841
1845
|
if (rank > worstRank) { worst = v; worstRank = rank; }
|
|
1842
1846
|
}
|
|
1843
|
-
|
|
1847
|
+
// Empty / all-unrecognized matched set → 'none' (a draft must not assert
|
|
1848
|
+
// 'unknown' exploitation it never observed).
|
|
1849
|
+
return worst || 'none';
|
|
1844
1850
|
}
|
|
1845
1851
|
|
|
1846
1852
|
// Severity ladder derived from rwep_adjusted. Playbooks reference
|
|
@@ -3838,4 +3844,6 @@ module.exports = {
|
|
|
3838
3844
|
_acquireLockDiagnostic: acquireLockDiagnostic,
|
|
3839
3845
|
_releaseLock: releaseLock,
|
|
3840
3846
|
_lockFilePath: lockFilePath,
|
|
3847
|
+
_vulnIdToUrn: vulnIdToUrn,
|
|
3848
|
+
_worstActiveExploitation: worstActiveExploitation,
|
|
3841
3849
|
};
|
|
@@ -62,7 +62,7 @@ function parseArgs(argv) {
|
|
|
62
62
|
'Usage: node lib/validate-catalog-meta.js [--quiet] [--strict]\n' +
|
|
63
63
|
'\n' +
|
|
64
64
|
' --quiet Suppress per-catalog PASS output; show failures only.\n' +
|
|
65
|
-
' --strict Promote
|
|
65
|
+
' --strict Promote freshness warnings to errors (used by the predeploy gate).\n',
|
|
66
66
|
);
|
|
67
67
|
process.exit(0);
|
|
68
68
|
} else {
|
|
@@ -165,11 +165,12 @@ function validateMeta(catalogPath, opts) {
|
|
|
165
165
|
|
|
166
166
|
/* freshness enforcement. When both meta.last_updated and
|
|
167
167
|
* freshness_policy.stale_after_days are present, surface a warning if
|
|
168
|
-
* (now - last_updated) > stale_after_days.
|
|
169
|
-
*
|
|
168
|
+
* (now - last_updated) > stale_after_days. Emitted at WARN level by
|
|
169
|
+
* default (does not fail validation).
|
|
170
170
|
*
|
|
171
171
|
* Optional `opts.strict` (or `opts.errorOnStale`) promotes the warning
|
|
172
|
-
* to an error
|
|
172
|
+
* to an error; the predeploy gate runs --strict, plain validation keeps
|
|
173
|
+
* the warning posture.
|
|
173
174
|
*/
|
|
174
175
|
if (
|
|
175
176
|
typeof meta.last_updated === 'string' &&
|
|
@@ -185,7 +186,7 @@ function validateMeta(catalogPath, opts) {
|
|
|
185
186
|
const msg =
|
|
186
187
|
`_meta freshness: last_updated ${meta.last_updated} is ${ageDays} days old ` +
|
|
187
188
|
`(stale_after_days = ${fp.stale_after_days}); refresh the catalog or bump _meta.last_updated. ` +
|
|
188
|
-
`
|
|
189
|
+
`Promoted to an error under --strict.`;
|
|
189
190
|
if (opts && (opts.strict || opts.errorOnStale)) {
|
|
190
191
|
errors.push(msg);
|
|
191
192
|
} else {
|