feat(security_audit): append-only security audit log + redaction (#440) [stacked on #476] - #477
Open
olegbrok wants to merge 1 commit into
Open
feat(security_audit): append-only security audit log + redaction (#440) [stacked on #476]#477olegbrok wants to merge 1 commit into
olegbrok wants to merge 1 commit into
Conversation
PR-6 of the #433 web-exposed hardening track. Introduces a dedicated security-event audit log distinct from the per-tool-call audit in hooks.AuditStore. Stacked on #476 → #475 → #474 → #473 → #472. Why a new store =============== hooks.AuditStore tracks tool execution (agent_name, session_id, tool_name, cost) — useful for cost/usage attribution. #440's audit log tracks security events (auth, admin, network reject, rate-limit, boot guards) with a different schema and a different retention story: * Security audit benefits from a focused export job that doesn't have to scan the bulky tool-call history. * Append-only by convention; no UPDATE/DELETE API. * Best-effort writes: a locked DB must NOT take down login. Schema ====== security_audit_log: id, ts, event_type, actor_user_id, actor_token_id, actor_ip, via_proxy, forwarded_chain (JSON), method, route_name, target, outcome, user_agent, correlation_id, detail_json, prev_hash -- reserved for future hash-chain integrity Indexes on (ts), (event_type, ts), (actor_user_id, ts), (correlation_id) — query patterns are time-windowed by event/actor filters or correlation-id traces across multi-hop requests. Redaction ========= Allowlist-shaped per event_type. Define what's *kept*, not what's stripped, so a future credential field name added to login flow defaults to redacted instead of accidentally leaking. EVENT_DETAIL_ALLOWLIST registers the allowed keys for known event types (auth.login.success/fail, auth.token.create/revoke, admin.*, rate_limit.exceeded, network.lan_filter.reject, boot.*). Unknown event types fall back to DEFAULT_DETAIL_ALLOWLIST — a conservative set ({reason, duration_ms, status_code, error_class, count, limit, bucket}). Best-effort writes ================== log_event catches all exceptions, logs at WARNING via the standard logger, returns None instead of raising. Boot-time refuse-to-boot events that need fail-loud semantics call assert_web_mode_safe (#441) which raises BootGuardError; the audit log there is informational only. Wiring ====== SecurityAuditStore is constructed in create_api alongside the existing AuditStore and cached on app.state.security_audit. Routes will reach it via Depends() once #435 / #438 / #439 / #443 land their consumers. Out of scope (deferred) ======================= * GET /audit endpoints (admin-only) — depend on #435 user accounts + #439 elevated-session protection. Land alongside those PRs. * GET /audit/export (CSV/JSON) — same dependency chain. * Retention cron wiring — store ships prune_older_than(); the scheduler wiring lands as a one-line follow-up once review confirms the desired default (365d). * Hash-chain population — column is reserved for the integrity work but not populated in this PR. * Override-emits-audit for boot guards (#441) — needs this PR + an obvious wiring point. Land in a follow-up that depends on both. Tests (25 new) ============== * redact_detail: None pass-through, per-event allowlist, default allowlist for unknown events, default doesn't include credential words, nested dict walked, lists walked, empty allowlist drops everything. * SecurityAuditStore: log returns id, redacts on write, query filters (event_type, actor, outcome, time range, correlation_id), newest-first ordering, pagination, count, via_proxy + forwarded_chain round-trip, method/route/user_agent round-trip, best-effort returns None on closed DB. * prune_older_than: deletes older rows, no-op on zero/negative. * Schema: table + all four indexes present, prev_hash column reserved. * Wiring: app.state.security_audit is a SecurityAuditStore instance; smoke-write through it. Closes part of #433. Refs #440. 🤖 Authored by Barsik
This was referenced May 13, 2026
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
PR-6 of the #433 web-exposed hardening track. Introduces a dedicated security-event audit log distinct from the per-tool-call audit in `hooks.AuditStore`.
Stacked on #476 → #475 → #474 → #473 → #472.
Closes part of #433. Refs #440.
Why a new store
`hooks.AuditStore` tracks tool execution (agent_name, session_id, tool_name, cost) — useful for cost/usage attribution. #440's audit log tracks security events (auth, admin, network reject, rate-limit, boot guards) with a different schema and retention story. Separate DB so a focused security-export job doesn't have to scan the bulky tool-call history.
Schema
`security_audit_log`:
Indexes on `(ts)`, `(event_type, ts)`, `(actor_user_id, ts)`, `(correlation_id)`.
Murzik review followups (from #440) — addressed
Out of scope (deferred to owning PRs)
Tests (25 new)
Test plan
🤖 Opened by Barsik