| name | build-audit-logging |
| description | Builds tamper-evident audit logging — structured actor/action/target/result records for security-relevant events, append-only hash-chained or WORM/object-lock storage, PII-safe payloads that log references not raw data, and regulation-driven retention — to satisfy SOC2/HIPAA-style controls and support incident forensics. |
| when_to_use | A system needs a defensible, queryable record of sensitive actions (access, permission/config changes, admin ops) for compliance or forensics. Distinct from observability-instrument (operational logs/metrics/traces for debugging) and map-privacy-data-gdpr (data-subject rights and lawful-basis mapping). |
When to Use
Reach for this skill when the requirement is a defensible record of who did what to whom, not operational telemetry:
- "We need an audit trail for SOC2 / HIPAA / PCI — access, admin actions, config changes"
- "Auditors want to know who changed this permission / exported this report / read this patient record"
- "After the breach, prove what the attacker touched and that nobody edited the logs"
- "Log every admin override / impersonation / data export, immutably"
- "Make sensitive-action history queryable for investigations and legal hold"
NOT this skill:
- Debugging latency/errors with logs, metrics, traces, dashboards → observability-instrument (operational, sampled, short-retention — the opposite of an audit log)
- Data-subject access/erasure requests, consent, lawful basis, retention policy for personal data → map-privacy-data-gdpr
- Deciding whether an action is allowed (the policy engine itself) → design-authorization-model (audit logging records the decision; it does not make it)
- An immutable append-only store as the system of record for business state (rebuildable projections) → design-event-sourcing-cqrs
- Storing/rotating the secrets and signing keys this log references → secrets-management
- Running the actual breach investigation/postmortem → incident-response-sre (this skill makes that investigation possible)
Steps
-
Enumerate auditable events first — code to a closed list, not "log everything." An audit log with too much noise is as useless as one with gaps. Audit exactly the security-relevant control points:
| Category | Examples | SOC2 (TSC) | HIPAA |
|---|
| Authentication | login success/fail, MFA, logout, password/key change, session revoke | CC6.1 | §164.312(b) |
| Authorization decisions | access denied, privilege grant/revoke, role change, impersonation start/stop | CC6.3 | §164.308(a)(4) |
| Sensitive data access | read/export/print of PII/PHI/financial records, bulk query, report download | CC6.1 / CC7.2 | §164.312(b) audit controls |
| Config / security changes | feature flag, retention policy, encryption setting, integration/webhook, IAM policy | CC8.1 | §164.308(a)(1) |
| Admin / break-glass ops | user delete, data purge, override, prod DB access, support impersonation | CC6.1 | §164.308(a)(3) |
Define this list with security/compliance, not ad hoc per feature. Each event gets a stable action constant (e.g. user.role.granted, record.exported) — never a free-text string you can't query or version.
-
Fix one structured schema and emit it everywhere. Required fields, machine-parseable (JSON), one shape across services:
{
"id": "01J8...ULID",
"ts": "2026-06-15T09:41:02.117Z",
"action": "record.exported",
"actor"
Common Errors
- Audit log shares the store/credentials with app logs. Anyone who can write debug logs can then forge or wipe audit history. Separate store, separate INSERT-only credential, separate retention.
- Logging raw PII/PHI or secrets in the payload. Creates a long-retention, broadly-read second copy of your crown jewels. Log ids and field-name diffs; scrub
meta against an allowlist before write.
- "Append-only" that the app account can still UPDATE/DELETE. That's not append-only. Revoke update/delete at the DB-role / bucket-policy level; verify with an attempted delete that must fail.
- Hash chain with no verification job. An undetected break = no tamper evidence at all. Run a scheduled verifier that recomputes the chain and alerts on the first mismatch; anchor the chain head externally if insiders are in scope.
- Async fire-and-forget emit. The action commits, the audit write is dropped on a queue overflow or crash, and you have a silent gap. Write in-transaction or via outbox; fail-closed for sensitive actions.
- Free-text
action strings. "User exported the data" can't be queried, aggregated, or mapped to a control. Use a versioned closed enum.
- Trusting client-supplied
X-Forwarded-For / actor id. Both are spoofable. Take source_ip only from the header your trusted proxy sets; take actor.id from the authenticated session, never from the request body.
- Missing impersonation provenance. Support acts "as" a user and the log shows only the end user — auditors flag this as a control gap. Always populate
on_behalf_of.
- Cron-job retention instead of store-enforced. A disabled or buggy cron either leaks data forever or deletes evidence early. Use object-lock / partition lifecycle so the store enforces it.
- No timezone discipline. Mixed local timestamps make a forensic timeline unreconstructable. UTC + ISO-8601 + ms, server clock, everywhere.
- Recording allows but dropping denies. Auditors and investigators care most about blocked attempts. Record
result: "deny" with reason, not just successful actions.
Verify
- Exactly-once coverage: For each event in the closed list, perform the action and confirm one audit record is written with all required fields populated; perform a sensitive action whose audit write is forced to fail and confirm the action is denied (fail-closed), not silently completed.
- Tamper detection: Directly mutate one stored row (or delete one), run the chain verifier → it flags the exact broken entry. Re-run on the untouched log → clean. This is the test that proves the tamper-evidence is real, not decorative.
- Immutability of the write path: As the application service account, attempt
UPDATE/DELETE on an audit row (and overwrite/delete on the object store) → both must be rejected by the role/bucket policy. Only INSERT/PutObject succeeds.
- No leakage: Trigger actions involving secrets and PII/PHI (export a record, change a password, edit a profile), then grep the stored audit entries for the raw secret, the password, and the literal PII values → zero hits; only ids, field names, and counts appear.
- Retention enforced by the store: Confirm the object-lock/partition policy is configured for the regulated window and that no role (including admin/root) can delete before expiry; confirm entries past the window expire automatically without a manual job.
- Investigation queries: Run "all actions by actor X in window T" and "all actors who touched target Y" → both return correct, complete results in interactive time on indexed fields, and a
request_id pivots to the matching operational trace.
- Provenance: An impersonated action shows both the acting agent and
on_behalf_of; a denied action shows result: "deny" + reason; source_ip matches the trusted-proxy value, not a spoofed body field.
Done = every event in the closed list emits exactly one complete record on a physically separate, INSERT-only, retention-locked store; the chain verifier detects any edit/delete; no secret or raw PII/PHI appears in any entry; and the two core investigation queries return complete, correct results mapped to their SOC2/HIPAA controls.