# ARGUS-A05: Audit Log Immutability > **Rule Code:** `ARGUS-A05` > **Identifier:** `AUDIT_LOG_IMMUTABILITY` > **Severity:** `CRITICAL` (Tampering & Non-Repudiation Blocker) > **Category:** `Security & Data Integrity` > **Target Standards:** CWE-284 (Improper Access Control), CWE-778 (Insufficient Logging), OWASP ASVS v4.0.3/v5.0 §V7.2.1, §V7.2.2, ISO 27001 / SOC 2 Type II --- ## 1. Overview & Core Invariant All audit trail tables (`audit_logs`, `security_events`, and configured ledger entities) **must remain strictly append-only**. Runtime application services and database migration scripts are strictly prohibited from executing modifying or destructive operations on audit tables: - **`UPDATE`** - **`DELETE`** - **`TRUNCATE`** - **`DROP TABLE`** - **`MERGE`** (specifically PostgreSQL 17/18 `WHEN MATCHED THEN UPDATE/DELETE`) - **Writable CTEs** (`WITH del AS (DELETE FROM audit_logs ...)`) The only permissible SQL Data Manipulation Language (DML) operations on audit tables are **`INSERT`** (recording events) and **`SELECT`** (forensic querying). --- ## 2. Technical Grounding & PostgreSQL Engine Realities ### 2.1. Forensic Integrity & Legal Non-Repudiation Audit trails provide tamper-evident proof of critical system events (e.g. citizen identity mutations, security authentications, and financial authorizations). Every mutating action is anchored with a publicly verifiable transaction identifier (`public_tx_id`). Allowing modifications or deletions destroys forensic non-repudiation, violating legal compliance regulations (GDPR, ISO 27001, SOC 2 Type II). ### 2.2. Multi-Layer Engine Defense (Role-Based Privileges) Static analysis by Argus must be paired with engine-level PostgreSQL privilege isolation: ```sql -- Runtime application role must only read and append REVOKE UPDATE, DELETE, TRUNCATE ON audit_logs FROM app_user; GRANT SELECT, INSERT ON audit_logs TO app_user; ``` ### 2.3. PostgreSQL 17/18 MERGE & Writable CTE Bypass Prevention PostgreSQL 17/18 SQL dialect supports `MERGE` commands and Writable Common Table Expressions (`WITH`). An attacker or misguided developer could attempt to alter logs using: - `MERGE INTO audit_logs USING ... WHEN MATCHED THEN UPDATE` - `WITH deleted AS (DELETE FROM audit_logs WHERE ...) SELECT 1` Argus performs deep recursive SQL AST inspection, analyzing root queries and nested CTEs alike. ```mermaid flowchart TD subgraph PERMITTED ["Append-Only Lifecycle (COMPLIANT)"] direction TB App1["Go Application Service"] -->|"INSERT INTO audit_logs"| DB1["PostgreSQL Engine"] DB1 -->|"Append-Only Storage"| Vault["Tamper-Proof Audit Vault (SAFE)"] Admin1["Auditor / Compliance"] -->|"SELECT FROM audit_logs"| Vault end subgraph PROHIBITED ["Destructive Mutation Vectors (BLOCKED)"] direction TB Tamper["Attacker / Coding Defect"] -->|"UPDATE / DELETE / TRUNCATE"| Block1["Argus Checker Static AST Guard"] Tamper -->|"MERGE INTO / Writable CTE"| Block1 Block1 -->|"CRITICAL Diagnostic"| Abort["Compilation / CI Pipeline Halted (CWE-284)"] end ``` ### 2.4. Data Retention via Partition Rotation (Never Bulk DELETE) Where data retention laws permit pruning audit records after statutory retention windows (e.g. 5 or 7 years), pruning **must never execute `DELETE FROM audit_logs`**, which causes table lockups and severe VACUUM bloat. Instead, retention is managed via partition rotation by scheduled administrative tooling: ```sql -- Administrative partition detachment (scheduled maintenance, NOT runtime code) ALTER TABLE audit_logs DETACH PARTITION audit_logs_y2020; DROP TABLE audit_logs_y2020; ``` --- ## 3. How Argus Detects Violations (Static Analysis Architecture) Argus inspects both Go application source code and SQL migration files: ```mermaid flowchart LR SQL["Extract SQL String
(Go Callsite or .up.sql)"] --> Parse["pg_query_go
PostgreSQL AST Parser"] Parse --> Traverse["Recursive AST Traversal
(Root Stmt & CTEs)"] Traverse --> TargetMatch{"Is Target Relation in
audit_tables allowlist?"} TargetMatch -->|Yes| OpCheck{"Is Operation
UPDATE, DELETE, TRUNCATE,
MERGE, or DROP?"} TargetMatch -->|No| Safe["Pass (Non-Audit Table)"] OpCheck -->|Yes| Report["Report CRITICAL Violation:
Audit Log Immutability"] OpCheck -->|No| Safe ``` 1. **Callsite Extraction:** Inspects all database execution sites (`Query`, `QueryRow`, `Exec`, `BeginTx`, `SendBatch`). 2. **Recursive AST Traversal (`ast_visitor.go`):** Evaluates `UpdateStmt`, `DeleteStmt`, `TruncateStmt`, `MergeStmt`, `DropStmt`, and traverses all `WithClause` CTEs. 3. **Configurable Target List:** Matches against `.argus.yaml` audit table definitions (`audit_logs`, `security_events`, or custom tables). 4. **Migration Scanner (`migration_check.go`):** Automatically analyzes all `.up.sql` migration scripts to ensure no destructive operations slip through database schema changes. --- ## 4. Vulnerability & Risk Taxonomy | Failure Mode | Technical Impact | Risk Severity | | :--------------------------------------- | :----------------------------------------------------------------------------- | :------------ | | **Audit Record Modification (`UPDATE`)** | Falsification of historical state, invalidating forensic evidence (CWE-284). | **CRITICAL** | | **Audit Record Deletion (`DELETE`)** | Erasure of malicious activity footprints, blinding security operations. | **CRITICAL** | | **Table Truncation (`TRUNCATE`)** | Total catastrophic erasure of enterprise audit history. | **CRITICAL** | | **MERGE Statement Tampering** | PostgreSQL 17/18 feature bypass updating records during synchronizations. | **CRITICAL** | | **Writable CTE Deletion** | Nested sub-statement bypass concealing destructive actions in `SELECT` bodies. | **CRITICAL** | --- ## 5. Non-Compliant Code Patterns (Bad Examples) ### Example 1: Updating an Audit Log Record ```go // VIOLATION: Attempting to modify audit payload func SanitizeLog(ctx context.Context, pool *pgxpool.Pool, logID string) error { // Flagged: forbidden UPDATE on audit table "audit_logs" query := "UPDATE audit_logs SET payload = '{}' WHERE id = $1" _, err := pool.Exec(ctx, query, logID) return err } ``` ### Example 2: Pruning Logs via Direct DELETE ```go // VIOLATION: Using runtime DELETE for cleanup func DeleteOldLogs(ctx context.Context, pool *pgxpool.Pool) error { // Flagged: forbidden DELETE on audit table "audit_logs" query := "DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL '30 days'" _, err := pool.Exec(ctx, query) return err } ``` ### Example 3: Circumventing via PostgreSQL MERGE ```go // VIOLATION: Using MERGE to mutate audit logs func SyncAuditEntries(ctx context.Context, pool *pgxpool.Pool) error { // Flagged: forbidden MERGE on audit table "audit_logs" query := ` MERGE INTO audit_logs a USING temp_sync t ON a.id = t.id WHEN MATCHED THEN UPDATE SET payload = t.payload ` _, err := pool.Exec(ctx, query) return err } ``` ### Example 4: Nested Deletion Inside Writable CTE ```go // VIOLATION: Hiding a DELETE inside a Common Table Expression func EvictWithCTE(ctx context.Context, pool *pgxpool.Pool, id string) error { // Flagged: forbidden DELETE on audit table "audit_logs" inside CTE! query := ` WITH deleted AS ( DELETE FROM audit_logs WHERE id = $1 ) SELECT 1 ` _, err := pool.Exec(ctx, query, id) return err } ``` --- ## 6. Compliant Implementation Patterns (Good Examples) ### Solution 1: Strictly Append-Only Recording (`INSERT`) ```go // COMPLIANT: Only appending new immutable audit event records func RecordAuditEvent(ctx context.Context, pool *pgxpool.Pool, event AuditEvent) error { const query = ` INSERT INTO audit_logs ( id, public_tx_id, actor_id, event_type, payload, ip_address, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, NOW() ) ` _, err := pool.Exec(ctx, query, event.ID, event.PublicTxID, event.ActorID, event.EventType, event.Payload, event.IPAddress, ) return err } ``` ### Solution 2: Forensic Analysis Reading (`SELECT`) ```go // COMPLIANT: Read-only query for compliance monitoring func GetAuditHistory(ctx context.Context, pool *pgxpool.Pool, entityID string) ([]AuditEvent, error) { const query = ` SELECT id, public_tx_id, actor_id, event_type, payload, created_at FROM audit_logs WHERE payload->>'entity_id' = $1 ORDER BY created_at DESC LIMIT 50 ` rows, err := pool.Query(ctx, query, entityID) if err != nil { return nil, err } defer rows.Close() return pgx.CollectRows(rows, pgx.RowToStructByName[AuditEvent]) } ``` --- ## 7. How to Suppress (Ignore Directives) For disaster recovery utilities or approved administrative repartitioning scripts: ```go // argus:ignore ARGUS-A05 authorized disaster recovery deduplication script _, err := pool.Exec(ctx, deduplicateQuery) ``` In SQL migration scripts: ```sql -- argus:ignore ARGUS-A05 emergency schema repartitioning TRUNCATE TABLE audit_logs; ``` Alternatively, use the identifier alias: ```go // argus:ignore AUDIT_LOG_IMMUTABILITY administrative archival migration _, err := pool.Exec(ctx, partitionMigrationQuery) ``` --- ## 8. Configuration Reference (`.argus.yaml`) Define audit tables in `.argus.yaml`: ```yaml rules: ARGUS-A05: enabled: true audit_tables: - audit_logs - security_events - auth_sessions_history ```