feat(audit): daily retention prune for audit_log entries past 365d (#552)#576
Merged
Conversation
) Closes the gap left by SEC-001 Phase 4: the append-only contract was enforced by SQLite triggers but nothing pruned aged-out rows, so audit_log grew unbounded. Adds a daily APScheduler job (`audit_retention_service.py`) that DELETEs `audit_log` rows older than `AUDIT_LOG_RETENTION_DAYS` (default 365). Floored at 365 because the `audit_log_no_delete` trigger refuses younger rows. Disable with `AUDIT_RETENTION_ENABLED=false`; runs at `AUDIT_RETENTION_HOUR:15` (default 04:15 UTC, one hour after log archival to spread DB writes). The prune SQL deliberately uses the same `datetime('now', ?)` form as the trigger's WHEN clause — this trades architectural invariant #16 (prefer `iso_cutoff()`) for trigger consistency, avoiding IntegrityError on day-of-cutoff boundary rows. Fixing the trigger to use ISO-Z form is tracked separately. Hash chain note: pruning DELETEs entries, breaking the SHA-256 chain across the cutoff. Verification ranges should stay within retention by design — documented in the service docstring. Tests cover: old-only removal, empty-table return, sub-365 floor rejection, and a boundary-row stress case that fails loudly if the WHERE clause and trigger ever drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
approved these changes
Apr 29, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
LGTM — well-patterned background service, invariant #16 deviation correctly justified, boundary-stress test covers the trigger/WHERE alignment. Approved.
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
Closes the gap left by SEC-001 Phase 4: the append-only audit log was protected by SQLite triggers but never pruned, so
audit_loggrew unbounded. Adds a daily APScheduler job that DELETEs entries older thanAUDIT_LOG_RETENTION_DAYS(default 365, floored at 365 to respect theaudit_log_no_deletetrigger floor).src/backend/services/audit_retention_service.py— mirrorslog_archive_serviceshape, runs at 04:15 UTC daily.PlatformAuditOperations.prune_audit_log()+db.prune_audit_log()facade.main.pylifespan start/stop.Why the SQL deliberately uses
datetime('now', ?)Architectural invariant #16 prefers
iso_cutoff()for rolling-window filters on ISO-Z TEXT columns. Here theaudit_log_no_deletetrigger's WHEN clause already usesdatetime('now', '-365 days')— using the same form in our WHERE clause keeps the prune set and the trigger's protected set in sync, avoidingIntegrityErroron day-of-cutoff boundary rows. The trigger has the underlying ISO-Z mismatch noted in invariant #16; fixing that is tracked separately to keep this PR scope-bounded.Hash chain note
Pruning DELETEs rows, which breaks the
previous_hash/entry_hashchain across the cutoff. Hash-chain verification ranges should stay inside the retention window by design. Documented in the service docstring; if hash chain is enabled and prune removes >0 rows, a structured warning is logged.Configuration
AUDIT_LOG_RETENTION_DAYS365AUDIT_RETENTION_ENABLEDtruefalseto disable the scheduler.AUDIT_RETENTION_HOUR4:15past.Closes #552.
Test plan
🤖 Generated with Claude Code