Best Practices

AI Rules for Audit Logging

AI logs nothing or logs everything with console.log. Rules for structured audit events, immutable audit trails, who/what/when/why capture, compliance-ready retention, and tamper-evident logging.

8 min read·March 8, 2025

Who accessed this record on March 15th? We do not know — console.log says 'order updated' with no context

Structured audit events, immutable trails, who/what/when/why, retention policies, tamper-evident hashing

AI Treats Audit Logging as an Afterthought

AI generates applications with: no audit logging (no record of who did what), console.log debugging (unstructured strings that cannot be queried), mutable logs (logs can be edited or deleted, destroying evidence), no context capture (the log says 'order updated' but not who updated it, what changed, or why), and no retention policy (logs either grow unbounded or are deleted too soon for compliance). When a security incident occurs or an auditor asks 'who accessed this record on March 15th?' — the answer is 'we do not know.'

Modern audit logging is: structured (JSON events with defined schema), immutable (append-only, no UPDATE or DELETE on audit records), contextual (who: user ID, what: action + resource, when: timestamp, why: reason/source), retention-managed (retained for compliance periods, archived then purged), and tamper-evident (checksums or hash chains detect unauthorized modifications). AI generates none of these.

These rules cover: structured audit event design, immutable append-only storage, who/what/when/why context capture, compliance retention policies, tamper-evident verification, and queryable audit search.

Rule 1: Structured Audit Event Schema

The rule: 'Every audit event follows a defined schema: { id: uuid, timestamp: ISO-8601, actor: { userId, email, ip, userAgent }, action: "order.updated", resource: { type: "order", id: "order-123" }, changes: { status: { from: "pending", to: "shipped" } }, metadata: { reason: "Customer requested express shipping", source: "dashboard", requestId: "req-abc" } }. The schema answers: who did it (actor), what happened (action + resource), what changed (changes diff), and why (metadata).'

For the changes diff: 'Capture before and after values for every modified field: changes: { status: { from: "pending", to: "shipped" }, shippingMethod: { from: "standard", to: "express" } }. This diff enables: undoing changes (restore the "from" values), understanding the full modification (not just "something changed"), and detecting suspicious patterns (who changed the price from $100 to $1?). The diff is computed in the application layer before the update is applied.'

AI generates: console.log('Order updated') — no who, no what changed, no when (well, maybe a timestamp in the log prefix), no why. Three months later: 'Who changed this order to shipped?' Grep the logs: 'Order updated' appears 10,000 times with no context. Structured audit events: filter by resource.id = 'order-123', see every change with actor, timestamp, and diff. The answer takes seconds, not hours.

  • Schema: id, timestamp, actor (who), action (what), resource (which), changes (diff), metadata (why)
  • Changes diff: { field: { from, to } } for every modified field
  • Actor includes: userId, email, IP address, userAgent — full attribution
  • Metadata: reason (why the change was made), source (dashboard, API, system), requestId
  • Queryable: filter by actor, action, resource, time range — not grep through text files
💡 Seconds, Not Hours

console.log('Order updated') appears 10,000 times with no context. Structured audit events: filter by resource.id = 'order-123', see every change with actor, timestamp, and field-level diff. Finding who changed what takes seconds, not hours of grep.

Rule 2: Immutable Append-Only Storage

The rule: 'Audit logs must be append-only: INSERT only, no UPDATE, no DELETE. Enforce at the database level: REVOKE UPDATE, DELETE ON audit_logs FROM app_user. The application database role can insert audit records but cannot modify or remove them. For additional protection: use a separate database or service for audit logs with its own access controls — even if the main database is compromised, the audit trail is intact.'

For why immutability matters: 'If audit logs can be modified, a malicious actor (or a compromised admin account) can: delete evidence of unauthorized access, modify timestamps to create alibis, change the actor field to frame someone else, or remove the entire audit trail. Immutable logs: the evidence is permanent. Even if the attacker has database access, they cannot alter the audit trail (they lack the DELETE/UPDATE privilege). The audit trail is the evidence — protect it like evidence.'

AI generates: audit logs in the same database, same schema, same access role as application data. An attacker who compromises the application can: read customer data AND delete the audit logs that recorded the breach. Separate storage with restricted access: the application inserts audit records via an API or queue. No direct database access. The audit service is a write-only endpoint — the application can send events but cannot query or modify them.

⚠️ Protect Evidence Like Evidence

Audit logs in the same database with the same access role: an attacker who compromises the app can delete the audit trail of the breach. Separate storage with INSERT-only access: the application can send audit events but cannot query or modify them. Evidence is permanent.

Rule 3: Who, What, When, Why Context Capture

The rule: 'Every audit event must answer four questions. WHO: the authenticated user (userId, email), the IP address, and the user agent. For system actions: actor = { type: "system", service: "scheduler" }. WHAT: the action performed (order.created, user.deleted, settings.updated) and the resource affected (type + ID). WHEN: ISO 8601 timestamp in UTC with millisecond precision. WHY: the reason for the action (user-provided reason for sensitive actions like account deletion, or the triggering event for system actions).'

For sensitive action reasons: 'For destructive or sensitive actions, require a reason: deleteUser(userId, { reason: "GDPR erasure request #4521" }). The reason is stored in the audit event metadata. When the auditor asks why a user was deleted, the answer is in the audit log: actor = admin@company.com, action = user.deleted, reason = GDPR erasure request #4521, timestamp = 2026-03-29T14:30:00Z. The reason connects the action to the business justification.'

AI generates: no reason capture. Admin deletes a user. Six months later: why was this user deleted? Was it a mistake? A policy violation? A GDPR request? Nobody knows. The audit log says 'user.deleted' with a timestamp — but the WHY is missing. Requiring a reason for destructive actions: the answer is always available, regardless of how much time has passed or whether the admin who performed the action still works at the company.

Rule 4: Compliance-Ready Retention Policies

The rule: 'Define retention periods per audit event category: security events (login, permission changes, data access): 7 years (SOC 2, HIPAA). Financial events (payments, refunds, invoices): 7 years (PCI DSS, tax requirements). User actions (CRUD operations, settings changes): 2 years (general compliance). System events (deployments, configuration changes): 1 year (operational). Archive to cold storage (S3 Glacier, Azure Archive) after the active query period. Purge after the retention period expires.'

For the retention pipeline: 'Active logs: in the primary database, queryable, indexed (last 90 days). Warm archive: moved to a time-series database or object storage, queryable with slightly higher latency (90 days to 1 year). Cold archive: compressed and moved to S3 Glacier or similar (1 year to retention limit). Purged: securely deleted after the retention period. Automate the pipeline: a scheduled job moves logs between tiers based on age. Never manually manage retention — automation ensures compliance.'

AI generates: no retention policy. Audit logs grow forever (storage costs increase linearly) or are deleted ad-hoc when disk space runs low (compliance violation — deleted logs may have been within the required retention period). A defined retention policy: storage costs are predictable (hot + warm + cold tiers), compliance is guaranteed (retention meets regulatory minimums), and purging is automated (no manual intervention, no accidental deletion).

  • Security events: 7 years (SOC 2, HIPAA) — login, permission changes, data access
  • Financial events: 7 years (PCI DSS, tax) — payments, refunds, invoices
  • User actions: 2 years — CRUD operations, settings changes
  • Three-tier storage: active (90 days, indexed), warm (1 year, queryable), cold (retention limit, archived)
  • Automated pipeline: scheduled job moves between tiers, purges after retention expires

Rule 5: Tamper-Evident Logging

The rule: 'Add tamper evidence to audit logs: each record includes a hash of the previous record (hash chain). Record N hash = SHA-256(Record N data + Record N-1 hash). Verifying integrity: rehash the chain from the beginning. If any record was modified or deleted, the chain breaks at that point. For stronger guarantees: periodically anchor the chain hash to an external system (blockchain timestamp, AWS QLDB, or a signed timestamping authority).'

For practical implementation: 'For most applications, a hash chain in the audit table is sufficient: audit_logs(id, ..., previous_hash, record_hash). On insert: compute record_hash = SHA-256(JSON.stringify(eventData) + previous_hash). A verification job runs daily: rehashes the chain, alerts if any hash mismatches. This catches: unauthorized modifications to audit records, deleted records (gap in the chain), and inserted records (hash does not match the chain). AWS QLDB provides this natively with a cryptographic journal.'

AI generates: audit logs with no integrity verification. A compromised admin modifies an audit record to cover their tracks. Without tamper evidence: the modification is undetectable. With a hash chain: the modified record hash does not match the chain. The daily verification job alerts: 'Audit chain integrity violation at record #45,231.' The tampering is detected, investigated, and the original data can potentially be recovered from backups.

ℹ️ Chain Breaks = Tampering Detected

Each audit record hashes the previous record. Modify one record: the chain breaks at that point. Delete a record: gap in the chain. A daily verification job rehashes and alerts on mismatches. Tampering is detected within 24 hours, every time.

Complete Audit Logging Rules Template

Consolidated rules for audit logging.

  • Structured schema: actor (who), action (what), resource (which), changes (diff), metadata (why)
  • Immutable storage: INSERT only, REVOKE UPDATE/DELETE, separate database or service
  • Four questions answered: who, what, when, why — every event, every time
  • Require reason for destructive actions: 'GDPR erasure request #4521' not just 'user.deleted'
  • Retention per category: security 7y, financial 7y, user actions 2y, system 1y
  • Three-tier storage pipeline: active (indexed) → warm (queryable) → cold (archived) → purge
  • Hash chain for tamper evidence: SHA-256(data + previous_hash), daily verification
  • AWS QLDB for native cryptographic journal — or hash chain in Postgres for most apps