Signed, hash-chained audit log for Warden decisions (v1.3.0)#25
Open
Ar9av wants to merge 1 commit into
Open
Conversation
Every decision (allow / observe / block) now lands in an append-only NDJSON
log at .prismor-warden/audit/<session_id>.ndjson. Records are linked by
SHA-256 hash chain and optionally Ed25519-signed, so a third party with the
public key can verify what the agent attempted, what was blocked, and why —
and prove the log has not been tampered with.
New modules:
- warden/signing.py — Ed25519 keygen / sign / verify, with cryptography
backend and openssl fallback.
- warden/audit_log.py — canonical encoder, hash chain writer, signing,
pinned policy/feed snapshots, chain verification, session sealing.
CLI: warden audit-log {keygen, pubkey, list, show, verify, seal,
register-pubkey, replay}. `verify` walks the chain, recomputes hashes, and
validates signatures, exiting non-zero on tampering.
Privacy: sensitive event fields (command, path, url, prompt, content,
response) and finding evidence are stored as SHA-256 digests + length, not
plaintext. Set `audit.include_raw: true` in policy.yaml to retain raw text.
Hash chaining is on by default (zero setup). Signing turns on automatically
once `warden audit-log keygen` has run, or when WARDEN_AUDIT_SIGNING_KEY is
set.
Tests cover: chain linkage, GENESIS handling, tamper detection via
record_hash and via dropped records, signature roundtrip, signature failure
on tampered records (including the "attacker recomputes hashes but lacks
the private key" case), policy/feed pinning to disk, redaction defaults,
and seal manifest creation. 31 new tests, full suite 258 pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a tamper-evident, optionally Ed25519-signed audit log for every decision Warden makes (allow / observe / block). This is the answer to the governance question raised externally — "are you generating a signed, replayable log of what gets blocked and why?" — with verification that doesn't require trusting the host.
warden/audit_log.py— append-only NDJSON log at.prismor-warden/audit/<session>.ndjson, hash-chained via SHA-256 of canonical bytes, with pinnedpolicy_hash+feed_hashsnapshots for replay.warden/signing.py— Ed25519 keygen / sign / verify; usescryptographyif installed, falls back toopenssl pkeyutl(same approach aspipeline/sign_feed.sh).warden audit-logCLI —keygen,pubkey,list,show,verify,seal,register-pubkey,replay.verifyexits non-zero on tampering.command,path,url,prompt,content,response) and finding evidence are stored as SHA-256 + length, not plaintext.audit.include_raw: trueinpolicy.yamlopts in to plaintext retention.warden audit-log keygen(or viaWARDEN_AUDIT_SIGNING_KEYenv).What an audit record looks like
```json
{
"v": 1, "alg": "sha256", "seq": 0,
"ts": "2026-05-03T19:03:24.139Z",
"session_id": "...", "agent": "claude", "mode": "enforce",
"event": { "type": "shell", "command_hash": "1de700c2...", "command_len": 6 },
"decision": "allow",
"findings": [],
"policy_hash": "a0066d3f...",
"feed_hash": "fb010dd8...",
"agent_version": "warden 1.3.0",
"prev_hash": "GENESIS",
"record_hash": "9286523ecb239dc5...",
"sig": { "alg": "ed25519", "key_id": "0d835451d9e089ff", "value": "" }
}
```
Threat model coverage
record_hashprev_hashmismatch andseqjump on the next recordTested explicitly in
tests/test_audit_log.py::TestVerify::test_verify_detects_full_recompute_attempt_without_key.Test plan
python3 -m unittest discover tests/— 258 tests pass (31 new)keygen→ 4 hook-dispatch payloads (allow/block/block/observe) →list/show/verifyclean → seal → flip a decision → verify FAILS with both chain break and bad signature, exit 2cryptography41.0.7) — all 14 checks passaudit-signer.keyis 0600,~/.prismor/keys/is 0700commandis not present in records by default; onlycommand_hash+command_lenOut of scope (follow-up)
record_hash+signature+key_idover the existing webhook/syslog/file sinks, so a SIEM can verify provenance independently. Tracked in a separate issue.Docs
docs/audit-log.md— record schema, privacy controls, CLI reference, off-host verification recipe.