feat(assurance): evidence freshness-SLA gate — catalog SLAs enforced against a digest-protected ledger (18th assurance check)
The OSCAL catalogs declare per-control freshness-sla props (env-01 PT5M,
cry-02/rte-01 P1D, con-07 P7D, cry-05 P3M, con-04 P1D/P90D) but check C3
only validated their FORMAT — nothing recorded when evidence was produced
and nothing failed when it went stale. The SLA was prose. This makes it
enforced.
New tool — governance_artifacts/check_evidence_freshness.py:
- --run executes every control's mapped runnable assurance check using the
SAME single-source-of-truth CONTROL_EVIDENCE map the regulator deliverable
generators use (no parallel drift-prone map) and writes a freshness ledger
(oscal/generated/evidence_freshness_ledger.json) recording pass/fail, the
UTC instant evidence was produced, and duration. Entries are protected by
a SHA-256 ledger digest over the canonical entries JSON.
- --audit enforces each catalog-declared SLA against those instants with
named, falsifiable checks: ledger-digest, evidence-recorded,
evidence-passed, evidence-fresh. Per-control statuses: FRESH / STALE /
FAILED / NOT-RECORDED / FUTURE-DATED / SLA-MISSING / SLA-MALFORMED /
NOT-RUNNABLE. Exit 0 iff every runnable control is recorded, passed and
fresh with an intact digest.
- Honest semantics: organisational-evidence controls (env-02) are disclosed
NOT-RUNNABLE, never counted fresh and never silently passed; a failed
check is never fresh; future-dated evidence (forged timestamp/clock skew)
fails; a missing ledger fails, never passes vacuously. Stated conversion
convention (1M=30d, 1Y=365d; compound P1D/P90D enforces the first period)
so audits are reproducible. The ledger digest detects casual edits, is
NOT a signature — pair with the ML-DSA-65-signed bundle (checks 16/17)
for signed provenance. --as-of enables reproducible point-in-time audits.
Wiring:
- run_runnable_assurance.sh: 17 -> 18 steps. Step 18 runs --run --audit
--print and asserts PASS, intact digest, zero failing controls, all
runnable controls FRESH, and >=1 organisational control disclosed.
- RUNNABLE_ASSURANCE.md: row 18 documents the gate and its honesty limits.
Tests (tests/governance/test_governance_artifacts.py — round 8, +7):
- SLA parser conventions (PT5M=300s, P3M=90d, P1D/P90D first period;
malformed P/PT/5M/P-1D/'' rejected).
- Fresh synthetic ledger -> PASS with env-02 disclosed NOT-RUNNABLE.
- Auditing 10 min later flips env-01 (PT5M) STALE and fails the gate while
cry-05 (P3M) stays FRESH — SLA granularity actually matters.
- Timestamp edit without re-digesting -> ledger-digest MISMATCH -> FAIL.
- passed=false is never FRESH; future-dated evidence FAILs; a control
missing from the ledger is NOT-RECORDED; missing ledger file FAILs.
- Single-source-of-truth check: every runnable CONTROL_EVIDENCE control
with a declared SLA is covered by the committed ledger.
Full suite result: 18/18 runnable assurance checks PASS; 37/37 governance
tests PASS.