An audit log that can be edited or deleted is not an audit log — it is a mutable ledger that any database administrator, compromised application role, or insider threat can silently rewrite. SOX §404 requires that internal controls over financial reporting be reliable; a log table with UPDATE or DELETE grants on the application role fails that test on its face. PCI-DSS 4.0 Req-10.3 requires protection of audit logs from destruction and unauthorized modification. FINRA Rule 4511 (book-and-records) requires that records be preserved in a non-rewriteable, non-erasable format. NIST 800-53 AU-9 mandates protection of audit information from unauthorized access, modification, and deletion. CWE-284 (Improper Access Control) is the vulnerability class. An attacker who gains write access to a role that can UPDATE transaction_logs can backdate or erase evidence of fund transfers with no trace.
Critical because a single UPDATE or DELETE on an audit row can permanently destroy the evidentiary chain of custody that regulators, courts, and forensic investigators rely on.
Explicitly REVOKE modification permissions in a migration under db/migrations/ or supabase/migrations/ — implicit defaults are not sufficient and do not count as pass:
REVOKE ALL ON transaction_logs FROM app_role;
GRANT INSERT, SELECT ON transaction_logs TO app_role;
-- Only a superuser/DBA role may ever touch rows
GRANT ALL ON transaction_logs TO dba_role;
For PostgreSQL with row-level security, add an INSERT-only policy and intentionally leave out UPDATE and DELETE policies so those operations are denied by default:
ALTER TABLE transaction_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY rls_insert_only ON transaction_logs
FOR INSERT TO app_role WITH CHECK (true);
-- No UPDATE/DELETE/TRUNCATE policy means those paths are blocked.
ID: finserv-audit-trail.transaction-logging.append-only-enforcement
Severity: critical
What to look for: Enumerate all database roles with access to the audit log table(s) and list all permissions granted. Count every role that has UPDATE, DELETE, or TRUNCATE permissions — quote the actual GRANT/REVOKE statements or RLS policies found. Check for column-level and row-level constraints that prevent modification. A role with UPDATE permission on even one column of the audit table must not pass — do not pass if any application role can modify rows.
Pass criteria: The audit log table(s) have explicit permissions that prevent UPDATE, DELETE, TRUNCATE operations from all application roles — at least 1 explicit REVOKE or RLS policy must be documented. Only INSERT and SELECT are permitted. Report the ratio of restricted vs. unrestricted roles (e.g., "3 of 3 app roles restricted to INSERT/SELECT only").
Fail criteria: The audit log table allows UPDATE or DELETE operations for any application role, or TRUNCATE is permitted without administrative override, or no explicit permission restrictions are documented (implicit defaults do not count as pass).
Skip (N/A) when: Never — append-only is fundamental to audit trails.
Detail on fail: Specify which operations are allowed on the audit log table. Example: "audit_logs table has UPDATE and DELETE permissions for app_role — 2 of 3 roles unrestricted" or "Database allows TRUNCATE on audit_logs without requiring superuser role".
Cross-reference: Check finserv-audit-trail.balance-reconciliation.access-control for role-based read restrictions, and finserv-audit-trail.retention-compliance.tamper-evidence for cryptographic tamper detection.
Remediation: Explicitly revoke modification permissions at the database level (in db/migrations/ or supabase/migrations/):
REVOKE ALL ON transaction_logs FROM app_role;
GRANT INSERT, SELECT ON transaction_logs TO app_role;
GRANT UPDATE, DELETE, TRUNCATE ON transaction_logs TO superuser_role ONLY;
For PostgreSQL with row-level security:
ALTER TABLE transaction_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY rls_append_only ON transaction_logs
FOR INSERT WITH CHECK (true);
-- No UPDATE, DELETE, or TRUNCATE policies
If using a document database (DynamoDB, Firestore), use similar immutability patterns: immutable document IDs, TTL-based archival only (no delete API), signed transaction records.