Claude
Skills
Sign in
Back

build-audit-logs

Included with Lifetime
$97 forever

Build or review audit trails in TypeScript/JavaScript apps using evlog (pipelines, typed actions, denials, retention, compliance-style reviews). For application code, not for extending the evlog package.

Security

What this skill does


# Build or Review an Audit System with evlog

For **application developers** who either need to add an audit trail to their product, or who already have one and want it reviewed. Walks through the design calls, the end-to-end implementation, and a review checklist for an existing setup.

This skill assumes the audit lives in **your app**. To extend the evlog package itself (new audit helper, new drain wrapper), see the contributor skills under `.agents/skills/`.

## Quick reference — call-site cheat sheet

When you already know the system is wired and just need to remember the API:

| Situation | Helper |
|---|---|
| Inside a request handler, action succeeded | `log.audit({ action, actor, target, outcome: 'success' })` |
| Inside a request handler, AuthZ denial | `log.audit.deny('reason', { action, actor, target })` |
| Standalone job / script / CLI (no request) | `audit({ action, actor, target, outcome })` |
| Auto-record success / failure / denied for a function | `withAudit({ action, target }, fn)` |
| Recording a state change | add `changes: auditDiff(before, after)` |
| Centralised typed action vocabulary | `defineAuditAction('invoice.refund', { target: 'invoice' })` |
| Asserting audits in tests | `mockAudit()` — `assertAudit()` or `toIncludeAuditOf()` |

`AuditFields` schema (always provide `action`, `actor`, `outcome`; `target` strongly recommended; the rest is filled in for you):

```ts
interface AuditFields {
  action: string                                  // 'invoice.refund'
  actor: { type: 'user' | 'system' | 'api' | 'agent', id: string, email?, displayName?, model?, tools?, reason?, promptId? }
  outcome: 'success' | 'failure' | 'denied'
  target?: { type: string, id: string, [k: string]: unknown }
  reason?: string
  changes?: { before?: unknown, after?: unknown, patch?: AuditPatchOp[] }
  causationId?: string
  correlationId?: string
  version?: number                                // defaults to AUDIT_SCHEMA_VERSION
  idempotencyKey?: string                         // auto-derived from action+actor+target+timestamp
  context?: { requestId?, traceId?, ip?, userAgent?, tenantId?, ... }   // filled by auditEnricher
  signature?: string                              // added by signed(drain, { strategy: 'hmac' })
  prevHash?: string                               // added by signed(drain, { strategy: 'hash-chain' })
  hash?: string                                   // added by signed(drain, { strategy: 'hash-chain' })
}
```

## What "audit logging" actually means

An audit log answers a forensic question: **who did what, on which resource, when, from where, with which outcome.** That's a different shape from observability logs, which is why the operational rules differ:

|                | Audit log                                       | Observability log                  |
| -------------- | ----------------------------------------------- | ---------------------------------- |
| Question       | "Who tried to do what, was it allowed?"         | "How did this request behave?"     |
| Sampling       | Never (force-keep)                              | Often (head + tail)                |
| Retention      | 1 – 7 years (compliance)                        | 30 – 90 days                       |
| Mutability     | Append-only, tamper-evident                     | Mutable, lossy                     |
| Audience       | Auditors, security, legal                       | Engineers                          |
| Storage        | Often dedicated (separate dataset / DB)         | Shared with telemetry              |

evlog ships the audit layer as a thin extension of its wide-event pipeline (a typed `audit` field on `BaseWideEvent` plus a few helpers and drain wrappers). The point is that you compose with the primitives the app already uses — same drains, same enrichers, same redact, same framework integration. There is no parallel system to maintain.

## Mental model

```text
log.audit(...) ──► sets event.audit ──► force-keep ──► auditEnricher ──► redact ──► every drain
                                                                                  └─► auditOnly(signed(fsDrain))
```

| Building block                              | Role                                                                | Required?               |
| ------------------------------------------- | ------------------------------------------------------------------- | ----------------------- |
| `log.audit()` / `audit()` / `withAudit()`   | Sets `event.audit` and force-keeps the event                        | Yes                     |
| `auditEnricher()`                           | Auto-fills `event.audit.context` (req / trace / ip / ua / tenantId) | Recommended             |
| `auditOnly(drain)`                          | Filters the drain to events with `event.audit` set                  | Recommended             |
| `signed(drain, ...)`                        | Adds tamper-evident integrity (HMAC or hash-chain)                  | Optional (compliance)   |
| `auditRedactPreset`                         | Strict PII preset for audit events                                  | Recommended             |
| `mockAudit()`                               | Captures audit events in tests                                      | Yes (in tests)          |

## Design calls before writing code

Make these explicit and write them down somewhere a security reviewer can find. Without a written rule, the system can't be audited — auditors look for the policy first, then the enforcement.

### 1. Where do audits live?

| Sink                              | Use when                                          | Trade-offs                                                                                  |
| --------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **FS** (`evlog/fs` + `signed`)    | Self-hosted, simple, you control the disk         | Manual rotation/backup; single-process unless you persist hash-chain `state` externally     |
| **Dedicated Axiom dataset**       | You already use Axiom                             | Easy queries, separate retention/billing; cost scales with volume                           |
| **Postgres / Neon / Aurora**      | You want SQL queries, joins with app data         | Need a schema, indexes, retention job; idempotency key prevents duplicates                  |
| **S3 + Object Lock**              | Append-only WORM compliance (HIPAA / FINRA)       | Read latency; pair with a queryable mirror (Athena)                                         |
| **Multiple sinks**                | Different audiences (engineers ↔ legal)           | Use `auditOnly` per sink; sinks fail in isolation by design                                 |

> **Rule of thumb.** Pick at least two: a queryable one (Axiom / Postgres) for day-to-day forensics + an append-only one (FS journal with hash-chain, or S3 Object Lock) as the compliance artefact. The two-drain pattern protects against vendor outages and admin mistakes on the queryable side.

### 2. Do you need integrity (`signed`)?

Yes if any of:

- A compliance framework requires tamper-evidence (SOC2 CC7, HIPAA §164.312(c)(1), PCI 10.5).
- The sink is mutable by engineers / admins.
- You may need to prove to a regulator that no events were modified after the fact.

Skip if:

- Sink is already WORM (S3 Object Lock, BigQuery append-only, Postgres with row-level immutability + monitored DDL).
- You're prototyping.

Strategies:

- `'hmac'` — per-event signature; quick to verify; rotate `secret` annually and embed a key id (extend `AuditFields`).
- `'hash-chain'` — sequence integrity; deleting a row breaks the chain forward; persist `state.{load,save}` if you run multiple processes (Redis is the typical store).

### 3. Multi-tenancy?

If the app is multi-tenant, **tenant isolation on every audit event is non-negotiab
Files: 2
Size: 26.6 KB
Complexity: 40/100
Category: Security

Related in Security