feat: SQLite immutable audit triggers - #113
Merged
Merged
Conversation
Install BEFORE UPDATE and BEFORE DELETE triggers on audit_log during schema init. Any modification or deletion of committed audit rows is aborted by SQLite at the engine level with zero runtime cost. The DELETE trigger is omitted when rotation (max_entries/max_age_days) is active since rotation intentionally prunes old rows. Tamper tests updated to drop triggers before mutating, demonstrating that the hash-chain remains a second layer of defence even if triggers are bypassed. Adds two new trigger unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
BEFORE UPDATE/BEFORE DELETEtriggers onaudit_logduring schema initialisation inSqliteAudit::with_rotation.no_audit_deletetrigger is skipped when rotation (max_entries/max_age_days) is configured, since rotation legitimately prunes old rows by design.verify_chain_detects_hash_mismatch,verify_chain_detects_chain_break) updated to drop triggers before mutating the DB, showing that the hash chain remains a second layer of defence even when triggers are bypassed by a privileged attacker.Closes #99
Test plan
cargo fmt --check— cleancargo clippy -- -D warnings— zero warningscargo test --lib— all 434 unit tests passtrigger_prevents_update_on_audit_log— UPDATE rejected with "immutable" errortrigger_prevents_delete_on_audit_log— DELETE rejected with "immutable" errormax_entries_rotation_keeps_newest— rotation still works (delete trigger skipped)max_age_days_rotation_purges_old— age rotation still works🤖 Generated with Claude Code