Repository navigation
feat(evidence): kates evidence export writes a period's record for an auditor - #565
Conversation
… auditor An auditor, as under the EU's DORA, asks what was tested in a period, when, by whom and how it went. This is Phase 3c of plans/enterprise.md, built in the CLI from endpoints that already exist, plus one server fix. - kates evidence export --from --to --out [--sign] writes a new directory: export.json (period, time, Kates URL), runs.json, disruptions.json (each full report), compliance-checks.json (each with its report), audit.json, audit-verify.json, and SHA256SUMS, so "shasum -a 256 -c SHA256SUMS" shows no file changed. Items are kept as the API wrote them. --sign runs cosign sign-blob keyless on SHA256SUMS. It exits 1 on a broken audit chain, with the pack written all the same. - Runs and disruptions have no period filter: the CLI pages them newest first, keeps [from, to), stops at the first older one, and keeps each id once if a new row shifts the pages. - GET /api/audit read at most its newest 500 rows and paged them in memory, so a busy period's export would have missed rows without a word. It now pages in the database, ordered by createdAt then id, answers the real total, and takes an exclusive until (400 when not an instant). The CLI refuses an audit list with rows outside the period, which is what an older Kates API answers. - The MCP caveat audit-actor-recorded and kates_activity's total no longer say "at most 500"; its anchored ref goes with the cap. - The book's CLI reference gains "Evidence for Auditors". Verified locally: mvnw verify (1788 unit, 98 IT on Postgres and Kafka), spotless:check, go test ./... (TestMCPCaveatRefAnchors included), go vet, golangci-lint --new-from-rev=origin/main, check-cli-style, check-cli-compat, check-book-style.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; no formal proof of the changed code.
Graphify review — findings
Adds kates evidence export, which writes a new directory with the runs, full disruption reports, stored compliance checks, audit rows and audit-chain verification for a [--from, --to) period, plus a SHA256SUMS manifest and optional keyless cosign signing via --sign. It creates the output directory up front and refuses one that already exists, keeps every item as the raw bytes the API returned through the new Client.GetRaw, pages with newestFirstIn and allPages, and rejects audit rows outside the period. When the audit chain is broken it still writes the pack and exits 1, and evidenceHint maps a 404 to an upgrade message for older APIs. It also registers evidence in commandProfiles with no profile and corrects the audit page comment so Total counts every matching row.
Worth a look
- allPages stops after the first page when the response has no total —
cli/cmd/evidence.go:222· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- A failed export leaves the output directory behind, so the retry is refused —
cli/cmd/evidence.go:64· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 731 functions depend on the 380 functions this change touches.
Health — this change adds coupling hotspots:
- new:
collectEvidence()— 1 callers, 6 callees - new:
evidenceExportCmd— 0 callers, 11 callees - new:
TestEvidenceExportPagesPastOnePageAndKeepsEachRunOnce()— 0 callers, 7 callees
Verification — 731 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 477 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
README.md§ Quick start (lines 54-99): references changed symbolskeyREADME.md§ Reach the services (lines 117-138): references changed symbolskeyREADME.md§ CLI at a glance (lines 148-170): references changed symbolskeyREADME.md§ GitHub Action (lines 171-203): references changed symbolskeyREADME.md§ Architecture (lines 208-222): references changed symbolskeycharts/connect-cluster/README.md§ Kafka connection (lines 41-66): references changed symbolskeycharts/connect-cluster/README.md§ Secrets (lines 106-112): references changed symbolskeycharts/connect-cluster/README.md§ Rails (lines 184-204): references changed symbolskeycharts/connect-cluster/README.md§ Values reference (lines 217-259): references changed symbolskeycharts/kafka-cluster/INSTALL.md§ Connecting applications (lines 155-185): references changed symbolskey
…and 10 more.
· 3 grounded finding(s) anchored inline below.
Review on #565: - allPages stopped once it had "total" items, so an answer without a total stopped after its first page: an export that drops rows without a word, the bug the PR fixes in /api/audit. It now reads to the first short page, and listPage no longer reads a total. - The directory was made before anything was read, so a failed export left it behind and the retry was refused. It is now checked first and made once everything is read, and a failed write removes it. Each has a test that fails on the code before this.
Why
An auditor, as under the EU's DORA, asks what was tested in a period, when, by whom and how it went, and whether the record was changed since. #560, #561 and #562 made Kates keep that record: stored compliance checks, a hash-chained audit log, and report hashes in the chain. This PR hands it over, as Phase 3c of the enterprise plan, built in the CLI from endpoints that already exist.
Building it turned up a silent data-loss bug:
GET /api/auditread at most its newest 500 rows and paged them in memory, so an export of a busy period would have missed rows without a word.What
kates evidence export --from <time> [--to <time>] --out <dir> [--sign]writes a new directory (an existing one is refused, so a pack never mixes with older files):export.jsonruns.jsondisruptions.jsoncompliance-checks.jsonreportSha256audit.jsonrowHashaudit-verify.jsonGET /api/audit/verify: whether the chain holds, and its headSHA256SUMSshasum -a 256 -c SHA256SUMSshows none changed--signrunscosign sign-blobkeyless onSHA256SUMS, in the foreground so the sign-in URL shows, and writesSHA256SUMS.sigstore.json.[from, to);--todefaults to now.GET /api/auditnow:createdAtthenid;total;until(exclusive; 400 when it isn't an instant).sincekeeps its old lenient behaviour.MCP: the caveat
audit-actor-recordedandkates_activity'stotalno longer say "at most 500". The anchored ref to the cap goes with it.Book: the CLI reference gains "Evidence for Auditors".
Deliberately not done
Risks
A run deleted during the export, as by the daily retention sweep, shifts the newest-first pages and can hide another run. The code marks it as a
shortcut:; a period filter on/api/testswould remove it.Tests
Locally:
./mvnw verify: 1788 unit tests and 98 ITs on real Postgres and Kafka, 0 failures, coverage gate passed. New:AuditVerifyTest: 520 rows read across 3 pages, past the old cap, anduntilexcludes.AuditResourceTest:untilpassed on; a baduntilis a 400.go test ./..., including 9 newTestEvidence*. Two came from review (7f611ff), and each fails on the code before it: all 401 audit rows are read across pages with nototalin the answer, and a failed export leaves no directory to block the retry. The others:SHA256SUMSmatches every file;go vet,golangci-lint --new-from-rev=origin/main(0 issues),check-cli-style.sh,check-cli-compat.sh,check-book-style.sh,spotless:check,TestMCPCaveatRefAnchors.Not run:
--signagainst real Sigstore, which needs a browser sign-in; and an export against a live Kates.