@blamejs/exceptd-skills 0.10.0 → 0.10.1
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 +54 -0
- package/bin/exceptd.js +303 -9
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/ai-api.json +400 -90
- package/data/playbooks/containers.json +406 -94
- package/data/playbooks/cred-stores.json +374 -89
- package/data/playbooks/crypto.json +369 -87
- package/data/playbooks/framework.json +376 -86
- package/data/playbooks/hardening.json +357 -84
- package/data/playbooks/kernel.json +324 -77
- package/data/playbooks/mcp.json +407 -92
- package/data/playbooks/runtime.json +345 -81
- package/data/playbooks/sbom.json +497 -111
- package/data/playbooks/secrets.json +352 -83
- package/lib/playbook-runner.js +77 -7
- package/lib/schemas/playbook.schema.json +5 -0
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.1 — 2026-05-12
|
|
4
|
+
|
|
5
|
+
**Patch: operator-reported bugs from v0.10.0 first contact + scope-aware `run` default.**
|
|
6
|
+
|
|
7
|
+
### New: `_meta.scope` + scope-aware multi-playbook `run`
|
|
8
|
+
|
|
9
|
+
Pre-0.10.1, `exceptd run` required a single explicit `<playbook>`. Operators had to know which of the 11 playbooks fit their context. Now:
|
|
10
|
+
|
|
11
|
+
- `exceptd run` (no args) auto-detects cwd: `.git/` → code playbooks; `/proc` + `/etc/os-release` → system playbooks. Always includes `cross-cutting`.
|
|
12
|
+
- `exceptd run --scope <type>` runs all playbooks matching `system | code | service | cross-cutting | all`.
|
|
13
|
+
- `exceptd run --all` runs every playbook.
|
|
14
|
+
- `exceptd run <playbook>` (explicit) keeps its existing behavior.
|
|
15
|
+
|
|
16
|
+
Each shipped playbook now carries `_meta.scope`:
|
|
17
|
+
- **system**: kernel · hardening · runtime · sbom · cred-stores
|
|
18
|
+
- **code**: secrets · containers
|
|
19
|
+
- **service**: mcp · ai-api · crypto
|
|
20
|
+
- **cross-cutting**: framework
|
|
21
|
+
|
|
22
|
+
Multi-playbook runs share one `session_id`; per-playbook attestations land under `.exceptd/attestations/<session_id>/<playbook_id>.json`. Aggregate output reports `summary.{succeeded, blocked, detected, inconclusive}`.
|
|
23
|
+
|
|
24
|
+
`exceptd plan` now groups output by scope by default with a `scope_summary` count. `--flat` returns the old flat list. `--scope <type>` filters.
|
|
25
|
+
|
|
26
|
+
### Bug fixes from operator first-contact
|
|
27
|
+
|
|
28
|
+
1. **Per-verb `--help` printed missing-arg errors.** `exceptd run --help` returned `{"ok":false,"error":"run: missing <playbookId> positional argument."}` instead of usage. Now every playbook verb (`plan`/`govern`/`direct`/`look`/`run`/`ingest`/`reattest`) honors `--help`/`-h` before positional validation and emits per-verb usage with flag descriptions, invocation modes, and `precondition_checks` submission shape.
|
|
29
|
+
|
|
30
|
+
2. **Preconditions were invisible to the host AI.** Neither `govern` nor `look` surfaced `_meta.preconditions`, so the AI couldn't see what facts to declare in its submission. `run` would then halt with `precondition_unverified` and the AI was blind. Fix: `look` response now includes `preconditions: [{id, check, on_fail, description}]` plus a `precondition_submission_shape` field giving the literal JSON shape (`{ "precondition_checks": { "<id>": true } }`) and an example. AGENTS.md updated.
|
|
31
|
+
|
|
32
|
+
3. **`precondition_checks` submission shape was undocumented in errors.** Preflight halt now returns a `remediation` field with the exact submission hint per failed precondition.
|
|
33
|
+
|
|
34
|
+
4. **`matched_cves` violated AGENTS.md Hard Rule #1.** Pre-0.10.1 output emitted `[{cve_id, rwep, cisa_kev, active_exploitation, ai_discovered}]` only — missing CVSS score/vector, KEV due date, PoC availability, AI-assisted-weaponization flag, patch availability, live-patch availability, EPSS, affected_versions, ATLAS/ATT&CK refs. The framework's own hard rule (every CVE reference must carry CVSS + KEV + PoC + AI-discovery + active-exploitation + patch/live-patch availability — theoretical-only is refused) was violated by the runner itself. Fix: `analyze.matched_cves[]` entries now carry all 15 required + optional Hard Rule #1 fields populated from the catalog. Null only when the catalog lacks the value, never when the runner forgot to forward.
|
|
35
|
+
|
|
36
|
+
5. **`detect.classification` ignored `signals.detection_classification`.** Agent could submit `{"detection_classification":"clean"}` with all-miss `signal_overrides` and still get `inconclusive`. Fix: agent override honored when set to `detected | inconclusive | not_detected | clean` (alias). Engine-computed classification used as fallback.
|
|
37
|
+
|
|
38
|
+
6. **`compliance_theater_check.verdict` stuck at `pending_agent_run` when classification was clear.** When the framework playbook ran with clean `detect.classification = not_detected`, the theater verdict still came back as pending instead of `clear`. Fix: when agent didn't submit `theater_verdict`, engine derives one from classification (`not_detected` → `clear`; otherwise `pending_agent_run`). Aliases `clean` / `no_theater` map to `clear`.
|
|
39
|
+
|
|
40
|
+
7. **No directive discoverability.** `exceptd plan` showed directive counts but not IDs/titles. Fix: `exceptd plan --directives` expands each playbook entry with `directives: [{id, title, applies_to}]`.
|
|
41
|
+
|
|
42
|
+
8. **No attestation inventory command.** Operators accumulated attestations under `.exceptd/attestations/` with no inventory verb; discovery required shell-globbing. Fix: new `exceptd list-attestations [--playbook <id>]` enumerates every prior session, sorted newest-first, with truncated evidence_hash + capture timestamp + file path.
|
|
43
|
+
|
|
44
|
+
### Deferred from operator report
|
|
45
|
+
|
|
46
|
+
These were noted in the same report and are scoped to v0.10.2 / v0.11:
|
|
47
|
+
|
|
48
|
+
- `framework-gap <framework> <cve-id>` named-framework filter doesn't match by gap-id prefix (carried over from v0.9.x).
|
|
49
|
+
- Crypto-codebase / library-internal playbook variant (new attack class for library authors).
|
|
50
|
+
- Framework-author operator persona (audit what you ship, not what you run).
|
|
51
|
+
- `reattest --latest <playbook>` / `--since <date>` (no need to know session-id).
|
|
52
|
+
- `run --diff-from-latest` for cron-driven baselines.
|
|
53
|
+
- `run --ci` exit-code-based gating for `.github/workflows/`.
|
|
54
|
+
- VEX consumption in sbom (`run sbom --vex vex.cdx.json` drops `known_not_affected` from analyze output).
|
|
55
|
+
- feeds_into threshold matrix documentation.
|
|
56
|
+
|
|
3
57
|
## 0.10.0 — 2026-05-11
|
|
4
58
|
|
|
5
59
|
**Minor: seven-phase playbook contract. exceptd becomes a knowledge layer that AI assistants consume, not a parallel scanner.**
|
package/bin/exceptd.js
CHANGED
|
@@ -101,7 +101,7 @@ const ORCHESTRATOR_PASSTHROUGH = new Set([
|
|
|
101
101
|
|
|
102
102
|
// Seven-phase playbook verbs handled in-process (no subprocess dispatch).
|
|
103
103
|
const PLAYBOOK_VERBS = new Set([
|
|
104
|
-
"plan", "govern", "direct", "look", "run", "ingest", "reattest",
|
|
104
|
+
"plan", "govern", "direct", "look", "run", "ingest", "reattest", "list-attestations",
|
|
105
105
|
]);
|
|
106
106
|
|
|
107
107
|
function readPkgVersion() {
|
|
@@ -149,8 +149,9 @@ Analyst:
|
|
|
149
149
|
|
|
150
150
|
Playbook runner — seven-phase contract
|
|
151
151
|
(govern → direct → look → detect → analyze → validate → close):
|
|
152
|
-
plan [--playbook id]... List playbooks + directives
|
|
153
|
-
[--
|
|
152
|
+
plan [--playbook id]... List playbooks + directives, grouped by scope.
|
|
153
|
+
[--scope system|code|service|cross-cutting|all]
|
|
154
|
+
[--flat] [--mode m] [--session-id id] [--pretty]
|
|
154
155
|
govern <playbook> Phase 1: GRC context (jurisdictions, theater,
|
|
155
156
|
framework gaps, skill_preload).
|
|
156
157
|
[--directive id] [--mode m] [--air-gap]
|
|
@@ -160,8 +161,14 @@ Playbook runner — seven-phase contract
|
|
|
160
161
|
look <playbook> Phase 3: artifact-collection spec the host AI
|
|
161
162
|
should execute.
|
|
162
163
|
[--directive id] [--air-gap]
|
|
163
|
-
run
|
|
164
|
-
|
|
164
|
+
run [playbook] Phases 4-7: detect → analyze → validate → close.
|
|
165
|
+
Three invocation modes:
|
|
166
|
+
run <playbook> single playbook (explicit)
|
|
167
|
+
run --scope <type> run all playbooks of that scope
|
|
168
|
+
run --all run every playbook
|
|
169
|
+
run auto-detect from cwd:
|
|
170
|
+
.git/ → code
|
|
171
|
+
/proc + os-release → system
|
|
165
172
|
[--directive id] [--evidence file|-]
|
|
166
173
|
[--session-id id] [--session-key hex]
|
|
167
174
|
[--force-stale] [--air-gap]
|
|
@@ -320,8 +327,15 @@ function firstDirectiveId(runner, playbookId) {
|
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
function dispatchPlaybook(cmd, argv) {
|
|
330
|
+
// Per-verb --help / -h before any positional-arg validation so users always
|
|
331
|
+
// get usage text instead of an error about missing arguments.
|
|
332
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
333
|
+
printPlaybookVerbHelp(cmd);
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
|
|
323
337
|
const args = parseArgs(argv, {
|
|
324
|
-
bool: ["pretty", "air-gap", "force-stale"],
|
|
338
|
+
bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives"],
|
|
325
339
|
multi: ["playbook"],
|
|
326
340
|
});
|
|
327
341
|
const pretty = !!args.pretty;
|
|
@@ -350,24 +364,167 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
350
364
|
case "run": return cmdRun(runner, args, runOpts, pretty);
|
|
351
365
|
case "ingest": return cmdIngest(runner, args, runOpts, pretty);
|
|
352
366
|
case "reattest": return cmdReattest(runner, args, runOpts, pretty);
|
|
367
|
+
case "list-attestations": return cmdListAttestations(runner, args, runOpts, pretty);
|
|
353
368
|
}
|
|
354
369
|
} catch (e) {
|
|
355
370
|
emitError(e.message, { verb: cmd }, pretty);
|
|
356
371
|
}
|
|
357
372
|
}
|
|
358
373
|
|
|
374
|
+
function printPlaybookVerbHelp(verb) {
|
|
375
|
+
const cmds = {
|
|
376
|
+
plan: `plan — list playbooks + directives, grouped by scope.
|
|
377
|
+
|
|
378
|
+
Flags:
|
|
379
|
+
--playbook <id> ... Filter to one or more playbook IDs.
|
|
380
|
+
--scope <type> Filter by scope: system | code | service | cross-cutting | all
|
|
381
|
+
--flat Disable grouped-by-scope output; emit flat list.
|
|
382
|
+
--directives Include directive id + title + applies_to per playbook.
|
|
383
|
+
--session-id <id> Reuse a specific session ID for the planning output.
|
|
384
|
+
--mode <m> Investigation mode forwarded into govern.
|
|
385
|
+
--pretty Indented JSON output.`,
|
|
386
|
+
govern: `govern <playbook> — phase 1, load GRC context for a playbook.
|
|
387
|
+
|
|
388
|
+
Args / flags:
|
|
389
|
+
<playbook> Playbook ID. Required positional.
|
|
390
|
+
--directive <id> Specific directive (default: first one).
|
|
391
|
+
--mode <m> Investigation mode forwarded into govern policy.
|
|
392
|
+
--air-gap Honor _meta.air_gap_mode + air_gap_alternative paths.
|
|
393
|
+
--pretty Indented JSON output.
|
|
394
|
+
|
|
395
|
+
Output: jurisdiction_obligations, theater_fingerprints, framework_context, skill_preload.`,
|
|
396
|
+
direct: `direct <playbook> — phase 2, threat context + skill chain + token budget.
|
|
397
|
+
|
|
398
|
+
Args / flags:
|
|
399
|
+
<playbook> Required positional.
|
|
400
|
+
--directive <id> Specific directive (default: first one).
|
|
401
|
+
--pretty Indented JSON output.`,
|
|
402
|
+
look: `look <playbook> — phase 3, artifact-collection spec the host AI executes.
|
|
403
|
+
|
|
404
|
+
Args / flags:
|
|
405
|
+
<playbook> Required positional.
|
|
406
|
+
--directive <id> Specific directive (default: first one).
|
|
407
|
+
--air-gap Honor air_gap_alternative paths.
|
|
408
|
+
--pretty Indented JSON output.
|
|
409
|
+
|
|
410
|
+
Output includes a 'preconditions' array — the host AI MUST verify each
|
|
411
|
+
precondition with its own probes and declare results back in the submission as:
|
|
412
|
+
{ "precondition_checks": { "<id>": true | false } }
|
|
413
|
+
The runner refuses the run if a precondition with on_fail=halt is unverified.`,
|
|
414
|
+
run: `run [playbook] — phases 4-7 (detect → analyze → validate → close).
|
|
415
|
+
|
|
416
|
+
Invocation modes:
|
|
417
|
+
run <playbook> Single playbook (explicit).
|
|
418
|
+
run --scope <type> Run all playbooks of that scope.
|
|
419
|
+
run --all Run every playbook.
|
|
420
|
+
run Auto-detect from cwd:
|
|
421
|
+
.git/ → code playbooks
|
|
422
|
+
/proc + os-release → system playbooks
|
|
423
|
+
Always includes cross-cutting playbooks.
|
|
424
|
+
|
|
425
|
+
Flags:
|
|
426
|
+
--directive <id> Specific directive (default: first one per playbook).
|
|
427
|
+
--evidence <file|-> Path to submission JSON or '-' for stdin.
|
|
428
|
+
Single-playbook shape:
|
|
429
|
+
{ artifacts, signal_overrides, signals, precondition_checks }
|
|
430
|
+
Multi-playbook shape:
|
|
431
|
+
{ "<playbook_id>": { artifacts, ... }, ... }
|
|
432
|
+
--session-id <id> Reuse a specific session ID.
|
|
433
|
+
--session-key <hex> HMAC sign the evidence_package with this key.
|
|
434
|
+
--force-stale Override the threat_currency_score < 50 hard-block.
|
|
435
|
+
--air-gap Honor air_gap_alternative paths.
|
|
436
|
+
--pretty Indented JSON output.
|
|
437
|
+
|
|
438
|
+
Attestation is persisted to .exceptd/attestations/<session_id>/ on every
|
|
439
|
+
successful run (single: attestation.json; multi: <playbook_id>.json).`,
|
|
440
|
+
ingest: `ingest — alias for 'run' matching AGENTS.md terminology.
|
|
441
|
+
|
|
442
|
+
Flags:
|
|
443
|
+
--domain <id> Playbook ID (overrides submission.playbook_id).
|
|
444
|
+
--directive <id> Directive ID (overrides submission.directive_id).
|
|
445
|
+
--evidence <file|-> Submission JSON. May include playbook_id/directive_id.
|
|
446
|
+
--pretty Indented JSON output.`,
|
|
447
|
+
reattest: `reattest <session-id> — replay a prior session and diff the evidence_hash.
|
|
448
|
+
|
|
449
|
+
Args / flags:
|
|
450
|
+
<session-id> Required positional. Looks under .exceptd/attestations/<id>/.
|
|
451
|
+
--pretty Indented JSON output.
|
|
452
|
+
|
|
453
|
+
Reports: unchanged | drifted | resolved from evidence_hash + classification deltas.`,
|
|
454
|
+
};
|
|
455
|
+
process.stdout.write((cmds[verb] || `${verb} — no per-verb help available; see \`exceptd help\` for the full list.`) + "\n");
|
|
456
|
+
}
|
|
457
|
+
|
|
359
458
|
function cmdPlan(runner, args, runOpts, pretty) {
|
|
360
|
-
|
|
459
|
+
let playbookIds = args.playbook
|
|
361
460
|
? (Array.isArray(args.playbook) ? args.playbook : [args.playbook])
|
|
362
461
|
: null;
|
|
462
|
+
// --scope filters playbook list by _meta.scope.
|
|
463
|
+
if (!playbookIds && args.scope) {
|
|
464
|
+
playbookIds = filterPlaybooksByScope(runner, args.scope);
|
|
465
|
+
}
|
|
363
466
|
const plan = runner.plan({
|
|
364
467
|
playbookIds: playbookIds || undefined,
|
|
365
468
|
mode: runOpts.mode,
|
|
366
469
|
session_id: runOpts.session_id,
|
|
367
470
|
});
|
|
471
|
+
// Default UX: group by scope unless --flat or a filter was applied.
|
|
472
|
+
if (!args.flat && !playbookIds) {
|
|
473
|
+
plan.grouped_by_scope = groupPlaybooksByScope(plan.playbooks);
|
|
474
|
+
plan.scope_summary = Object.fromEntries(
|
|
475
|
+
Object.entries(plan.grouped_by_scope).map(([s, list]) => [s, list.length])
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
// --directives expands each playbook entry with its directive id + title +
|
|
479
|
+
// applies_to so operators / AIs can pick a specific directive without
|
|
480
|
+
// grepping playbook source.
|
|
481
|
+
if (args.directives) {
|
|
482
|
+
for (const pb of plan.playbooks) {
|
|
483
|
+
const full = runner.loadPlaybook(pb.id);
|
|
484
|
+
pb.directives = full.directives.map(d => ({ id: d.id, title: d.title, applies_to: d.applies_to }));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
368
487
|
emit(plan, pretty);
|
|
369
488
|
}
|
|
370
489
|
|
|
490
|
+
function filterPlaybooksByScope(runner, scope) {
|
|
491
|
+
const ids = runner.listPlaybooks();
|
|
492
|
+
return ids.filter(id => {
|
|
493
|
+
try {
|
|
494
|
+
const pb = runner.loadPlaybook(id);
|
|
495
|
+
return scope === "all" || pb._meta.scope === scope;
|
|
496
|
+
} catch { return false; }
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function groupPlaybooksByScope(playbooks) {
|
|
501
|
+
const groups = {};
|
|
502
|
+
for (const pb of playbooks) {
|
|
503
|
+
const scope = pb.scope || pb._meta?.scope || "unscoped";
|
|
504
|
+
(groups[scope] = groups[scope] || []).push(pb.id);
|
|
505
|
+
}
|
|
506
|
+
return groups;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Auto-detect which scopes apply to the cwd. Returns an array of scope strings.
|
|
511
|
+
* - `code` when the cwd looks like a git repo
|
|
512
|
+
* - `system` when /proc + /etc/os-release exist (Linux host)
|
|
513
|
+
* - `service` always included as advisory — service investigations don't
|
|
514
|
+
* depend on cwd; the operator/AI decides whether to run them
|
|
515
|
+
*
|
|
516
|
+
* Returns at minimum `['cross-cutting']` so framework correlation can always
|
|
517
|
+
* run after other findings land.
|
|
518
|
+
*/
|
|
519
|
+
function detectScopes() {
|
|
520
|
+
const detected = [];
|
|
521
|
+
if (fs.existsSync(path.join(process.cwd(), ".git"))) detected.push("code");
|
|
522
|
+
if (fs.existsSync("/proc") && fs.existsSync("/etc/os-release")) detected.push("system");
|
|
523
|
+
// service playbooks need explicit invocation — they have side effects
|
|
524
|
+
// (probing remote endpoints) so we don't auto-include them.
|
|
525
|
+
return detected.length ? detected : ["cross-cutting"];
|
|
526
|
+
}
|
|
527
|
+
|
|
371
528
|
function cmdGovern(runner, args, runOpts, pretty) {
|
|
372
529
|
const playbookId = args._[0];
|
|
373
530
|
if (!playbookId) return emitError("govern: missing <playbookId> positional argument.", null, pretty);
|
|
@@ -396,8 +553,32 @@ function cmdLook(runner, args, runOpts, pretty) {
|
|
|
396
553
|
}
|
|
397
554
|
|
|
398
555
|
function cmdRun(runner, args, runOpts, pretty) {
|
|
399
|
-
const
|
|
400
|
-
|
|
556
|
+
const positional = args._[0];
|
|
557
|
+
|
|
558
|
+
// Multi-playbook dispatch path. Triggered by --all, --scope <type>, or by
|
|
559
|
+
// a bare `exceptd run` (no positional, no flags) which auto-detects scopes
|
|
560
|
+
// from the cwd.
|
|
561
|
+
if (!positional && (args.all || args.scope)) {
|
|
562
|
+
let ids;
|
|
563
|
+
if (args.all) {
|
|
564
|
+
ids = runner.listPlaybooks();
|
|
565
|
+
} else {
|
|
566
|
+
ids = filterPlaybooksByScope(runner, args.scope);
|
|
567
|
+
}
|
|
568
|
+
return cmdRunMulti(runner, ids, args, runOpts, pretty, { trigger: args.all ? "--all" : `--scope ${args.scope}` });
|
|
569
|
+
}
|
|
570
|
+
if (!positional && !args.all && !args.scope) {
|
|
571
|
+
const scopes = detectScopes();
|
|
572
|
+
const ids = scopes.flatMap(s => filterPlaybooksByScope(runner, s));
|
|
573
|
+
const unique = [...new Set(ids)];
|
|
574
|
+
if (unique.length === 0) {
|
|
575
|
+
return emitError("run: no playbook resolved. Pass <playbookId>, --scope <type>, or --all.", null, pretty);
|
|
576
|
+
}
|
|
577
|
+
return cmdRunMulti(runner, unique, args, runOpts, pretty, { trigger: "auto-detect", detected_scopes: scopes });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Single-playbook path (existing behavior).
|
|
581
|
+
const playbookId = positional;
|
|
401
582
|
const pb = runner.loadPlaybook(playbookId);
|
|
402
583
|
const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
|
|
403
584
|
if (!directiveId) return emitError(`run: playbook ${playbookId} has no directives.`, null, pretty);
|
|
@@ -446,6 +627,81 @@ function cmdRun(runner, args, runOpts, pretty) {
|
|
|
446
627
|
emit(result, pretty);
|
|
447
628
|
}
|
|
448
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Multi-playbook run. Iterates `ids` (already filtered by scope or auto-detect),
|
|
632
|
+
* runs each through runner.run with a shared session_id, persists each
|
|
633
|
+
* attestation under .exceptd/attestations/<session_id>/<playbook_id>.json, and
|
|
634
|
+
* emits a single aggregate bundle. Refuses if no evidence is provided (the
|
|
635
|
+
* host AI MUST submit observations per playbook — the engine can't synthesize them).
|
|
636
|
+
*
|
|
637
|
+
* Evidence shape for multi-run: { <playbook_id>: { artifacts, signal_overrides, signals, precondition_checks } }
|
|
638
|
+
* Falls back to running every playbook with empty evidence (engine returns
|
|
639
|
+
* inconclusive findings + visibility gaps) when no --evidence is given.
|
|
640
|
+
*/
|
|
641
|
+
function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
|
|
642
|
+
const sessionId = runOpts.session_id || require("crypto").randomBytes(8).toString("hex");
|
|
643
|
+
runOpts.session_id = sessionId;
|
|
644
|
+
|
|
645
|
+
let bundle = {};
|
|
646
|
+
if (args.evidence) {
|
|
647
|
+
try { bundle = readEvidence(args.evidence); } catch (e) {
|
|
648
|
+
return emitError(`run: failed to read evidence bundle: ${e.message}`, { evidence: args.evidence }, pretty);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const results = [];
|
|
653
|
+
for (const id of ids) {
|
|
654
|
+
const pb = runner.loadPlaybook(id);
|
|
655
|
+
const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
|
|
656
|
+
if (!directiveId) {
|
|
657
|
+
results.push({ playbook_id: id, ok: false, error: "no directives" });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const submission = bundle[id] || {};
|
|
661
|
+
const perRunOpts = { ...runOpts };
|
|
662
|
+
if (submission.precondition_checks) perRunOpts.precondition_checks = submission.precondition_checks;
|
|
663
|
+
|
|
664
|
+
const result = runner.run(id, directiveId, submission, perRunOpts);
|
|
665
|
+
|
|
666
|
+
// Persist per-playbook attestation under the shared session.
|
|
667
|
+
if (result && result.ok) {
|
|
668
|
+
try {
|
|
669
|
+
const dir = path.join(process.cwd(), ".exceptd", "attestations", sessionId);
|
|
670
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
671
|
+
fs.writeFileSync(
|
|
672
|
+
path.join(dir, `${id}.json`),
|
|
673
|
+
JSON.stringify({
|
|
674
|
+
session_id: sessionId,
|
|
675
|
+
playbook_id: id,
|
|
676
|
+
directive_id: directiveId,
|
|
677
|
+
evidence_hash: result.evidence_hash,
|
|
678
|
+
submission,
|
|
679
|
+
run_opts: { airGap: perRunOpts.airGap, forceStale: perRunOpts.forceStale, mode: perRunOpts.mode },
|
|
680
|
+
captured_at: new Date().toISOString(),
|
|
681
|
+
}, null, 2)
|
|
682
|
+
);
|
|
683
|
+
} catch { /* non-fatal */ }
|
|
684
|
+
}
|
|
685
|
+
results.push(result);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
emit({
|
|
689
|
+
ok: results.every(r => r.ok !== false),
|
|
690
|
+
session_id: sessionId,
|
|
691
|
+
trigger: meta.trigger,
|
|
692
|
+
detected_scopes: meta.detected_scopes || null,
|
|
693
|
+
playbooks_run: ids,
|
|
694
|
+
summary: {
|
|
695
|
+
total: results.length,
|
|
696
|
+
succeeded: results.filter(r => r.ok !== false).length,
|
|
697
|
+
blocked: results.filter(r => r.ok === false).length,
|
|
698
|
+
detected: results.filter(r => r.phases?.detect?.classification === "detected").length,
|
|
699
|
+
inconclusive: results.filter(r => r.phases?.detect?.classification === "inconclusive").length,
|
|
700
|
+
},
|
|
701
|
+
results,
|
|
702
|
+
}, pretty);
|
|
703
|
+
}
|
|
704
|
+
|
|
449
705
|
function cmdIngest(runner, args, runOpts, pretty) {
|
|
450
706
|
// `ingest` matches the AGENTS.md ingest contract. The submission JSON may
|
|
451
707
|
// carry playbook_id + directive_id; --domain/--directive flags override.
|
|
@@ -580,6 +836,44 @@ function cmdReattest(runner, args, runOpts, pretty) {
|
|
|
580
836
|
}, pretty);
|
|
581
837
|
}
|
|
582
838
|
|
|
839
|
+
function cmdListAttestations(runner, args, runOpts, pretty) {
|
|
840
|
+
const root = path.join(process.cwd(), ".exceptd", "attestations");
|
|
841
|
+
if (!fs.existsSync(root)) {
|
|
842
|
+
return emit({ ok: true, attestations: [], note: `No attestations directory at ${path.relative(process.cwd(), root)}` }, pretty);
|
|
843
|
+
}
|
|
844
|
+
const sessions = fs.readdirSync(root, { withFileTypes: true })
|
|
845
|
+
.filter(d => d.isDirectory())
|
|
846
|
+
.map(d => d.name);
|
|
847
|
+
|
|
848
|
+
const entries = [];
|
|
849
|
+
for (const sid of sessions) {
|
|
850
|
+
const sdir = path.join(root, sid);
|
|
851
|
+
const files = fs.readdirSync(sdir).filter(f => f.endsWith(".json"));
|
|
852
|
+
for (const f of files) {
|
|
853
|
+
try {
|
|
854
|
+
const j = JSON.parse(fs.readFileSync(path.join(sdir, f), "utf8"));
|
|
855
|
+
// Apply --playbook filter if supplied.
|
|
856
|
+
if (args.playbook && j.playbook_id !== args.playbook) continue;
|
|
857
|
+
entries.push({
|
|
858
|
+
session_id: sid,
|
|
859
|
+
playbook_id: j.playbook_id,
|
|
860
|
+
directive_id: j.directive_id,
|
|
861
|
+
evidence_hash: j.evidence_hash ? j.evidence_hash.slice(0, 16) + "..." : null,
|
|
862
|
+
captured_at: j.captured_at || null,
|
|
863
|
+
file: path.relative(process.cwd(), path.join(sdir, f)),
|
|
864
|
+
});
|
|
865
|
+
} catch { /* skip malformed */ }
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
entries.sort((a, b) => (b.captured_at || "").localeCompare(a.captured_at || ""));
|
|
869
|
+
emit({
|
|
870
|
+
ok: true,
|
|
871
|
+
attestations: entries,
|
|
872
|
+
count: entries.length,
|
|
873
|
+
filter: { playbook: args.playbook || null },
|
|
874
|
+
}, pretty);
|
|
875
|
+
}
|
|
876
|
+
|
|
583
877
|
if (require.main === module) main();
|
|
584
878
|
|
|
585
879
|
module.exports = { COMMANDS, PKG_ROOT, PLAYBOOK_VERBS };
|
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-12T13:36:15.948Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 49,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "f71c110a187ffaf19db2bbbb1f15ceb27f88bbb844d81329c21ba6705badac8a",
|
|
8
8
|
"data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
|
|
9
9
|
"data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
|
|
10
10
|
"data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
|