@blamejs/exceptd-skills 0.10.1 → 0.10.2
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/AGENTS.md +51 -0
- package/CHANGELOG.md +41 -0
- package/bin/exceptd.js +145 -6
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/crypto-codebase.json +1387 -0
- package/data/playbooks/kernel.json +1 -1
- package/data/playbooks/library-author.json +1792 -0
- package/lib/framework-gap.js +17 -1
- package/lib/playbook-runner.js +49 -4
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/AGENTS.md
CHANGED
|
@@ -77,6 +77,57 @@ Operator asks: "is this host vulnerable to Copy Fail?" AI invokes `node lib/play
|
|
|
77
77
|
|
|
78
78
|
Schema reference: `lib/schemas/playbook.schema.json`. Reference playbook (read this before authoring a new one): `data/playbooks/kernel.json`.
|
|
79
79
|
|
|
80
|
+
### feeds_into threshold matrix
|
|
81
|
+
|
|
82
|
+
Each playbook's `_meta.feeds_into[]` declares downstream playbooks the host AI should consider chaining into after this run, and the condition that fires the chain. The condition expressions evaluate at `close()` against `analyze` + `validate` + `agentSignals` context. AI assistants surface the suggested next playbook to the operator but never auto-execute; the operator decides.
|
|
83
|
+
|
|
84
|
+
The current (v0.10.x) matrix:
|
|
85
|
+
|
|
86
|
+
| From | Triggers | To | Why |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| ai-api | `analyze.compliance_theater_check.verdict == 'theater'` | framework | dotfile-cred-exposure theater pattern |
|
|
89
|
+
| ai-api | `analyze.blast_radius_score >= 4` | sbom | broad blast radius → inventory check |
|
|
90
|
+
| ai-api | `finding.includes_mcp_server_credential_exposure == true` | mcp | MCP creds leaked → MCP fleet audit |
|
|
91
|
+
| containers | `finding.severity >= 'high'` | kernel | container escape → kernel surface |
|
|
92
|
+
| containers | `always` | secrets | manifests routinely embed secrets |
|
|
93
|
+
| cred-stores | `finding.severity >= 'high'` | secrets | leaked creds in store → repo grep |
|
|
94
|
+
| cred-stores | `finding.severity == 'critical'` | runtime | critical exposure → listening-surface audit |
|
|
95
|
+
| crypto | `analyze.compliance_theater_check.verdict == 'theater'` | framework | FIPS-claim vs reality |
|
|
96
|
+
| crypto | `analyze.blast_radius_score >= 4` | sbom | crypto blast → SBOM-cve match |
|
|
97
|
+
| framework | `any compliance_theater_check.verdict == 'theater' AND blast_radius_score >= 4` | sbom | theater + breadth → inventory |
|
|
98
|
+
| hardening | `always` | kernel | hardening is corroborator for kernel finding |
|
|
99
|
+
| hardening | `finding.severity >= 'high'` | runtime | weak hardening → check actual exposure |
|
|
100
|
+
| kernel | `finding.severity == 'critical' OR analyze.blast_radius_score >= 4` | sbom | critical kernel → SBOM cross-ref |
|
|
101
|
+
| kernel | `analyze.compliance_theater_check.verdict == 'theater'` | framework | patch-SLA theater |
|
|
102
|
+
| mcp | `finding.severity == 'critical' OR analyze.blast_radius_score >= 4` | sbom | broad MCP impact → inventory |
|
|
103
|
+
| mcp | `analyze.compliance_theater_check.verdict == 'theater'` | framework | MCP-trust theater |
|
|
104
|
+
| mcp | `finding.includes_credential_exposure == true` | ai-api | MCP cred → AI-API cred audit |
|
|
105
|
+
| runtime | `always` | kernel | listener finding always informs kernel triage |
|
|
106
|
+
| runtime | `always` | hardening | runtime exposure pairs with hardening posture |
|
|
107
|
+
| runtime | `finding.severity == 'critical' OR analyze.blast_radius_score >= 3` | cred-stores | critical runtime → check cred stores |
|
|
108
|
+
| sbom | `analyze.compliance_theater_check.verdict == 'theater'` | framework | SBOM-signing theater |
|
|
109
|
+
| sbom | `any matched_cve.attack_class == 'kernel-lpe'` | kernel | kernel CVE in inventory → kernel playbook |
|
|
110
|
+
| sbom | `any matched_cve.attack_class == 'mcp-supply-chain'` | mcp | MCP CVE in inventory → MCP playbook |
|
|
111
|
+
| sbom | `any matched_cve.attack_class IN ['ai-c2', 'prompt-injection']` | ai-api | AI CVE → AI-API playbook |
|
|
112
|
+
| secrets | `finding.severity >= 'high'` | cred-stores | leaked secret in repo → check store posture |
|
|
113
|
+
|
|
114
|
+
Cross-cutting playbook `framework` is the natural correlation layer — many playbooks chain into it on a theater verdict. `sbom` is the breadth-of-impact follow-up most playbooks suggest when blast radius crosses 4. `kernel` + `hardening` + `runtime` form a tightly-coupled triangle (any one finding raises questions in the other two). When a playbook lists `always` as a feeds_into condition, the chain runs unconditionally — the AI should always at least offer the next playbook to the operator.
|
|
115
|
+
|
|
116
|
+
### CLI reference
|
|
117
|
+
|
|
118
|
+
| Verb | What it does |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `exceptd plan` | Default: grouped-by-scope summary of all 13 playbooks. `--scope <type>` filters. `--directives` expands directive IDs/titles per playbook. `--flat` for non-grouped. |
|
|
121
|
+
| `exceptd govern <pb>` | Phase 1 — jurisdiction obligations, theater fingerprints, framework gaps, skills to preload. |
|
|
122
|
+
| `exceptd direct <pb>` | Phase 2 — threat context, RWEP thresholds, skill chain, token budget. |
|
|
123
|
+
| `exceptd look <pb>` | Phase 3 — typed artifact-collection spec + `preconditions` array + submission-shape hint. |
|
|
124
|
+
| `exceptd run <pb>` | Phases 4-7 from agent evidence. Auto-detect cwd when no playbook positional. `--scope <type>` or `--all` for multi-playbook. `--vex <file>` to drop CycloneDX/OpenVEX `not_affected` CVEs. `--ci` for exit-code gating. `--diff-from-latest` for drift mode. `--force-stale` to override currency hard-block. |
|
|
125
|
+
| `exceptd ingest` | Alias for `run`; submission JSON may carry `playbook_id` + `directive_id`. |
|
|
126
|
+
| `exceptd reattest [<sid> \| --latest]` | Replay prior session, diff evidence_hash. `--latest [--playbook <id>] [--since <ISO>]` finds the most recent attestation automatically. |
|
|
127
|
+
| `exceptd list-attestations` | Inventory `.exceptd/attestations/<sid>/` — every prior session, newest first. `--playbook <id>` filters. |
|
|
128
|
+
|
|
129
|
+
All verbs support `--help` for per-verb usage. JSON output by default; `--pretty` for indented.
|
|
130
|
+
|
|
80
131
|
---
|
|
81
132
|
|
|
82
133
|
## Recurring Drift Rules
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.2 — 2026-05-12
|
|
4
|
+
|
|
5
|
+
**Patch: v0.10.1 deferred set — framework-gap filter fix, VEX consumption, CI gating, drift mode, 2 new playbooks (13 total), feeds_into matrix.**
|
|
6
|
+
|
|
7
|
+
### Bug fix (carried from v0.9.x)
|
|
8
|
+
|
|
9
|
+
**`exceptd framework-gap NIST-800-53 <cve-id>` returned 0 matches** while `framework-gap all <cve-id>` correctly found the same gap. Root cause: catalog stores `g.framework = "NIST SP 800-53 Rev 5"` (spaces) but operators pass `NIST-800-53` (hyphens), and `.includes()` is case + format sensitive. Fix: normalize both sides via `.toLowerCase().replace(/[\s_-]/g, '')` then substring-match against `g.framework` value AND prefix-match against the gap KEY (e.g. `NIST-800-53-SI-2`).
|
|
10
|
+
|
|
11
|
+
### New CLI flags
|
|
12
|
+
|
|
13
|
+
- **`run --vex <file>`** — load a CycloneDX or OpenVEX document. CVEs marked `not_affected | resolved | false_positive` (CycloneDX) or `not_affected | fixed` (OpenVEX) drop out of `analyze.matched_cves`. Dropped CVEs surface under `analyze.vex.dropped_cves` so the disposition is preserved for the audit trail.
|
|
14
|
+
- **`run --ci`** — machine-readable verdict for CI gates. Exits 2 when `phases.detect.classification === 'detected'` OR (`classification === 'inconclusive'` AND `rwep.adjusted >= rwep_threshold.escalate`). Logs PASS/FAIL reason to stderr. Pure not_detected runs exit 0 even when the playbook's catalogued CVEs carry high baseline RWEP — the gate is about the host-specific verdict, not the catalog.
|
|
15
|
+
- **`run --diff-from-latest`** — compare evidence_hash against the most recent prior attestation for the same playbook in `.exceptd/attestations/`. Drift mode for cron baselines. Result includes `prior_session_id`, `prior_captured_at`, `prior_evidence_hash`, `new_evidence_hash`, `status: unchanged | drifted | no_prior_attestation_for_playbook`.
|
|
16
|
+
- **`reattest --latest [--playbook <id>] [--since <ISO>]`** — find the most-recent attestation automatically. No session-id required.
|
|
17
|
+
|
|
18
|
+
### New playbooks (12 → 13)
|
|
19
|
+
|
|
20
|
+
- **`crypto-codebase`** (scope: code, attack_class: pqc-exposure) — complements the host-side `crypto` playbook. Walks the codebase for in-source crypto choices: weak hash imports (MD5/SHA1), `Math.random()` in security context, PBKDF2 iteration counts, ECDSA curve choices, RSA bit-size constants, PQC adoption signals. Theater fingerprints include `pqc-ready-feature-flag-without-ml-kem` (config toggle with zero ML-KEM call sites), `fips-validated-by-linking-openssl` (link-time vs runtime FIPS provider), `pbkdf2-iterations-set-in-2015` (10k defaults in published packages).
|
|
21
|
+
- **`library-author`** (scope: code, attack_class: supply-chain) — audits what you SHIP, not what you run. Vendored deps, SBOM signing posture, SLSA provenance attestation, VEX issuance, npm provenance, Rekor entries, cosign signing, branch protection, OIDC vs static publish tokens, EU CRA Art.13/14 conformity. Distinct from `sbom` (install-side); this is publish-side. Mutex with `secrets` since both compete for repo-walk cycles.
|
|
22
|
+
|
|
23
|
+
### feeds_into threshold matrix (v0.10.2 doc pass)
|
|
24
|
+
|
|
25
|
+
AGENTS.md now ships the full feeds_into matrix — 25 chains across 12 playbooks. Documents what triggers what, so operators understand the suggested-next-playbook routing rather than treating it as opaque magic. Highlights:
|
|
26
|
+
|
|
27
|
+
- `framework` is the natural correlation layer — many playbooks chain into it on `analyze.compliance_theater_check.verdict == 'theater'`.
|
|
28
|
+
- `sbom` is the breadth-of-impact follow-up most playbooks suggest when `analyze.blast_radius_score >= 4`.
|
|
29
|
+
- `kernel + hardening + runtime` form a tightly-coupled triangle (any one raises questions in the other two).
|
|
30
|
+
- `always` conditions on `hardening → kernel`, `runtime → kernel`, `runtime → hardening`, `containers → secrets` — the AI should always at least offer the next playbook to the operator.
|
|
31
|
+
|
|
32
|
+
### Internal
|
|
33
|
+
|
|
34
|
+
- **kernel.json feeds_into typo fix** — `compliance-theater` referent (no such playbook ID) corrected to `framework` (the playbook carrying the compliance-theater attack class). Test updated to assert the corrected chain.
|
|
35
|
+
- **`vexFilterFromDoc` helper** in `lib/playbook-runner.js` — parses CycloneDX VEX or OpenVEX documents into a `Set<string>` of CVE IDs whose disposition is "not_affected" or equivalent.
|
|
36
|
+
- **AGENTS.md** — new "feeds_into threshold matrix" section + "CLI reference" table.
|
|
37
|
+
|
|
38
|
+
### Still deferred (next pass)
|
|
39
|
+
|
|
40
|
+
- crypto-codebase playbook ships `eu-ai-act` and `cmmc` in `frameworks_in_scope` but doesn't thread either into `framework_gap_mapping` — Hard Rule #4 (no orphaned references) tidy. Either drop the entries or add concrete mapping in a follow-up.
|
|
41
|
+
- Crypto-codebase byte size (95 KB) is above the 50-60 KB target for new playbooks — load-bearing content but worth a depth audit.
|
|
42
|
+
- `_meta.feeds_into[].condition` parser supports a limited DSL — some playbooks use expressions like `any matched_cve.attack_class IN ['ai-c2', 'prompt-injection']` that the current parser doesn't fully support. Conditions degrade silently to false. Worth a parser pass to either expand the DSL or warn on unknown shapes.
|
|
43
|
+
|
|
3
44
|
## 0.10.1 — 2026-05-12
|
|
4
45
|
|
|
5
46
|
**Patch: operator-reported bugs from v0.10.0 first contact + scope-aware `run` default.**
|
package/bin/exceptd.js
CHANGED
|
@@ -335,7 +335,7 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
const args = parseArgs(argv, {
|
|
338
|
-
bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives"],
|
|
338
|
+
bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives", "ci", "latest", "diff-from-latest"],
|
|
339
339
|
multi: ["playbook"],
|
|
340
340
|
});
|
|
341
341
|
const pretty = !!args.pretty;
|
|
@@ -429,6 +429,18 @@ Flags:
|
|
|
429
429
|
{ artifacts, signal_overrides, signals, precondition_checks }
|
|
430
430
|
Multi-playbook shape:
|
|
431
431
|
{ "<playbook_id>": { artifacts, ... }, ... }
|
|
432
|
+
--vex <file> Load a CycloneDX or OpenVEX document. CVEs marked
|
|
433
|
+
not_affected | resolved | false_positive (CycloneDX)
|
|
434
|
+
or not_affected | fixed (OpenVEX) drop out of
|
|
435
|
+
analyze.matched_cves. The disposition is preserved
|
|
436
|
+
under analyze.vex.dropped_cves.
|
|
437
|
+
--diff-from-latest Compare evidence_hash against the most recent prior
|
|
438
|
+
attestation for the same playbook in
|
|
439
|
+
.exceptd/attestations/. Emits status: unchanged | drifted.
|
|
440
|
+
--ci Machine-readable verdict for CI gates. Exits non-zero
|
|
441
|
+
(code 2) when phases.detect.classification === 'detected'
|
|
442
|
+
OR phases.analyze.rwep.adjusted >= rwep_threshold.escalate.
|
|
443
|
+
Logs PASS/FAIL reason to stderr.
|
|
432
444
|
--session-id <id> Reuse a specific session ID.
|
|
433
445
|
--session-key <hex> HMAC sign the evidence_package with this key.
|
|
434
446
|
--force-stale Override the threat_currency_score < 50 hard-block.
|
|
@@ -444,13 +456,24 @@ Flags:
|
|
|
444
456
|
--directive <id> Directive ID (overrides submission.directive_id).
|
|
445
457
|
--evidence <file|-> Submission JSON. May include playbook_id/directive_id.
|
|
446
458
|
--pretty Indented JSON output.`,
|
|
447
|
-
reattest: `reattest <session-id> — replay a prior session and diff the evidence_hash.
|
|
459
|
+
reattest: `reattest [<session-id> | --latest] — replay a prior session and diff the evidence_hash.
|
|
448
460
|
|
|
449
461
|
Args / flags:
|
|
450
|
-
<session-id>
|
|
462
|
+
<session-id> Looks under .exceptd/attestations/<id>/attestation.json.
|
|
463
|
+
--latest Find the most-recent attestation automatically.
|
|
464
|
+
--playbook <id> Restrict --latest to a specific playbook.
|
|
465
|
+
--since <ISO> Restrict --latest to attestations after this ISO 8601 timestamp.
|
|
451
466
|
--pretty Indented JSON output.
|
|
452
467
|
|
|
453
468
|
Reports: unchanged | drifted | resolved from evidence_hash + classification deltas.`,
|
|
469
|
+
"list-attestations": `list-attestations [--playbook <id>] — enumerate prior attestations.
|
|
470
|
+
|
|
471
|
+
Args / flags:
|
|
472
|
+
--playbook <id> Filter to one playbook.
|
|
473
|
+
--pretty Indented JSON output.
|
|
474
|
+
|
|
475
|
+
Lists every attestation under .exceptd/attestations/<session_id>/, sorted
|
|
476
|
+
newest-first, with truncated evidence_hash + capture timestamp + file path.`,
|
|
454
477
|
};
|
|
455
478
|
process.stdout.write((cmds[verb] || `${verb} — no per-verb help available; see \`exceptd help\` for the full list.`) + "\n");
|
|
456
479
|
}
|
|
@@ -598,6 +621,19 @@ function cmdRun(runner, args, runOpts, pretty) {
|
|
|
598
621
|
runOpts.precondition_checks = submission.precondition_checks;
|
|
599
622
|
}
|
|
600
623
|
|
|
624
|
+
// --vex <file>: load a CycloneDX/OpenVEX document and pass the not_affected
|
|
625
|
+
// CVE ID set through to analyze() so matched_cves drops them.
|
|
626
|
+
if (args.vex) {
|
|
627
|
+
try {
|
|
628
|
+
const vexDoc = JSON.parse(fs.readFileSync(args.vex, "utf8"));
|
|
629
|
+
const vexSet = runner.vexFilterFromDoc(vexDoc);
|
|
630
|
+
submission.signals = submission.signals || {};
|
|
631
|
+
submission.signals.vex_filter = [...vexSet];
|
|
632
|
+
} catch (e) {
|
|
633
|
+
return emitError(`run: failed to load --vex ${args.vex}: ${e.message}`, null, pretty);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
601
637
|
const result = runner.run(playbookId, directiveId, submission, runOpts);
|
|
602
638
|
|
|
603
639
|
// Persist attestation for reattest cycles when the run succeeded.
|
|
@@ -624,6 +660,68 @@ function cmdRun(runner, args, runOpts, pretty) {
|
|
|
624
660
|
process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
|
|
625
661
|
process.exit(1);
|
|
626
662
|
}
|
|
663
|
+
|
|
664
|
+
// --diff-from-latest: compare evidence_hash against the most recent prior
|
|
665
|
+
// attestation for this playbook. Drift mode for cron baselines.
|
|
666
|
+
// We've already persisted the CURRENT attestation above, so the find must
|
|
667
|
+
// skip our session_id to get the actual prior one.
|
|
668
|
+
if (args["diff-from-latest"] && result && result.evidence_hash) {
|
|
669
|
+
const prior = findLatestAttestation({ playbookId, excludeSessionId: result.session_id });
|
|
670
|
+
if (prior) {
|
|
671
|
+
const priorHash = prior.parsed.evidence_hash;
|
|
672
|
+
result.diff_from_latest = {
|
|
673
|
+
prior_session_id: prior.parsed.session_id,
|
|
674
|
+
prior_captured_at: prior.parsed.captured_at,
|
|
675
|
+
prior_evidence_hash: priorHash,
|
|
676
|
+
new_evidence_hash: result.evidence_hash,
|
|
677
|
+
status: priorHash === result.evidence_hash ? "unchanged" : "drifted",
|
|
678
|
+
};
|
|
679
|
+
} else {
|
|
680
|
+
result.diff_from_latest = { status: "no_prior_attestation_for_playbook", playbook_id: playbookId };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// --ci: machine-readable verdict for CI gates.
|
|
685
|
+
//
|
|
686
|
+
// The detect phase classification is the host-specific signal — "is THIS
|
|
687
|
+
// environment exploitable for the catalogued CVEs". rwep.base is the
|
|
688
|
+
// worst-known catalog score for the domain, which is roughly constant
|
|
689
|
+
// regardless of the local environment; we don't fail CI on that alone or
|
|
690
|
+
// every CI run against a domain with KEV-listed catalog entries would
|
|
691
|
+
// perma-fail.
|
|
692
|
+
//
|
|
693
|
+
// Verdict:
|
|
694
|
+
// detected → FAIL (exit 2)
|
|
695
|
+
// inconclusive + rwep ≥ escalate
|
|
696
|
+
// → FAIL (the agent's evidence raised RWEP)
|
|
697
|
+
// not_detected → PASS (exit 0)
|
|
698
|
+
// inconclusive + rwep < escalate
|
|
699
|
+
// → PASS with WARN to stderr (visibility gap)
|
|
700
|
+
if (args.ci && result && result.phases) {
|
|
701
|
+
const classification = result.phases.detect && result.phases.detect.classification;
|
|
702
|
+
const rwep = result.phases.analyze && result.phases.analyze.rwep;
|
|
703
|
+
const threshold = rwep && rwep.threshold && rwep.threshold.escalate;
|
|
704
|
+
const adjusted = rwep && typeof rwep.adjusted === "number" ? rwep.adjusted : 0;
|
|
705
|
+
const escalate = typeof threshold === "number" && adjusted >= threshold;
|
|
706
|
+
|
|
707
|
+
emit(result, pretty);
|
|
708
|
+
|
|
709
|
+
if (classification === "detected") {
|
|
710
|
+
process.stderr.write(`[exceptd run --ci] FAIL: classification=detected rwep=${adjusted} threshold=${threshold}\n`);
|
|
711
|
+
process.exit(2);
|
|
712
|
+
}
|
|
713
|
+
if (classification === "inconclusive" && escalate) {
|
|
714
|
+
process.stderr.write(`[exceptd run --ci] FAIL: classification=inconclusive AND rwep=${adjusted} >= threshold=${threshold}\n`);
|
|
715
|
+
process.exit(2);
|
|
716
|
+
}
|
|
717
|
+
if (classification === "inconclusive") {
|
|
718
|
+
process.stderr.write(`[exceptd run --ci] PASS+WARN: classification=inconclusive rwep=${adjusted} < threshold=${threshold} (visibility gap)\n`);
|
|
719
|
+
} else {
|
|
720
|
+
process.stderr.write(`[exceptd run --ci] PASS: classification=${classification} rwep=${adjusted}\n`);
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
627
725
|
emit(result, pretty);
|
|
628
726
|
}
|
|
629
727
|
|
|
@@ -760,11 +858,52 @@ function cmdIngest(runner, args, runOpts, pretty) {
|
|
|
760
858
|
emit(result, pretty);
|
|
761
859
|
}
|
|
762
860
|
|
|
861
|
+
/**
|
|
862
|
+
* Find the latest attestation file under .exceptd/attestations/.
|
|
863
|
+
* Filters: optional playbook ID and optional "since" ISO timestamp.
|
|
864
|
+
* Returns { sessionId, playbookId, file, parsed } or null.
|
|
865
|
+
*/
|
|
866
|
+
function findLatestAttestation(opts = {}) {
|
|
867
|
+
const root = path.join(process.cwd(), ".exceptd", "attestations");
|
|
868
|
+
if (!fs.existsSync(root)) return null;
|
|
869
|
+
const sessions = fs.readdirSync(root, { withFileTypes: true })
|
|
870
|
+
.filter(d => d.isDirectory())
|
|
871
|
+
.map(d => d.name);
|
|
872
|
+
const candidates = [];
|
|
873
|
+
for (const sid of sessions) {
|
|
874
|
+
const sdir = path.join(root, sid);
|
|
875
|
+
for (const f of fs.readdirSync(sdir).filter(x => x.endsWith(".json"))) {
|
|
876
|
+
try {
|
|
877
|
+
const p = path.join(sdir, f);
|
|
878
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
879
|
+
if (opts.playbookId && j.playbook_id !== opts.playbookId) continue;
|
|
880
|
+
if (opts.since && (j.captured_at || "") < opts.since) continue;
|
|
881
|
+
if (opts.excludeSessionId && sid === opts.excludeSessionId) continue;
|
|
882
|
+
candidates.push({ sessionId: sid, playbookId: j.playbook_id, file: p, parsed: j });
|
|
883
|
+
} catch { /* skip malformed */ }
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
candidates.sort((a, b) => (b.parsed.captured_at || "").localeCompare(a.parsed.captured_at || ""));
|
|
887
|
+
return candidates[0] || null;
|
|
888
|
+
}
|
|
889
|
+
|
|
763
890
|
function cmdReattest(runner, args, runOpts, pretty) {
|
|
764
|
-
|
|
765
|
-
|
|
891
|
+
// --latest [--playbook <id>] [--since <ISO>] — find prior attestation
|
|
892
|
+
// without requiring the operator to know the session-id.
|
|
893
|
+
let sessionId = args._[0];
|
|
894
|
+
let attFile = null;
|
|
895
|
+
if (!sessionId && args.latest) {
|
|
896
|
+
const found = findLatestAttestation({
|
|
897
|
+
playbookId: args.playbook ? (Array.isArray(args.playbook) ? args.playbook[0] : args.playbook) : null,
|
|
898
|
+
since: args.since || null,
|
|
899
|
+
});
|
|
900
|
+
if (!found) return emitError("reattest: --latest found no matching attestations.", { filter: { playbook: args.playbook || null, since: args.since || null } }, pretty);
|
|
901
|
+
sessionId = found.sessionId;
|
|
902
|
+
attFile = found.file;
|
|
903
|
+
}
|
|
904
|
+
if (!sessionId) return emitError("reattest: missing <session-id>. Pass a session-id or --latest [--playbook <id>] [--since <ISO>].", null, pretty);
|
|
766
905
|
const dir = path.join(process.cwd(), ".exceptd", "attestations", sessionId);
|
|
767
|
-
|
|
906
|
+
if (!attFile) attFile = path.join(dir, "attestation.json");
|
|
768
907
|
if (!fs.existsSync(attFile)) {
|
|
769
908
|
return emitError(`reattest: no attestation found at ${path.relative(process.cwd(), attFile)}`, { session_id: sessionId }, pretty);
|
|
770
909
|
}
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-12T13:
|
|
3
|
+
"generated_at": "2026-05-12T13:50:32.319Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 49,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "3614ad87835688365283c95a8b7af1d66f14928c07efef750c206ba2f13aab33",
|
|
8
8
|
"data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
|
|
9
9
|
"data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
|
|
10
10
|
"data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
|