Skip to main content

build-audit-logs

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.

Zur Installation springen

Quellinformationen

Repository
evloghq/evlog
Letzte Quellaktivität
8. September 2026 um 17:53
Erkannte Sprache von SKILL.md
Englisch
Sterne
1.855
Forks
63

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
build-audit-logs
description
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.
# 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 | `defineAuditCatalog('billing', { INVOICE_REFUND: { target: 'invoice' } })` — or `defineAuditAction('invoice.refund', { target: 'invoice' })` for one-offs | | 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? | Drain | 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 drains** | Different audiences (engineers ↔ legal) | Use `auditOnly` per drain; drains 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 drain is mutable by engineers / admins. - You may need to prove to a regulator that no events were modified after the fact. Skip if: - Drain 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-negotiable.** A query that mixes tenants is a privacy incident. Wire it once in the enricher: ```ts auditEnricher({ tenantId: ctx => resolveTenant(ctx) }) ``` If the app uses Better Auth, `auditEnricher` can also bridge the authenticated session into `audit.context`. See the Better Auth integration (`evlog/better-auth`, https://www.evlog.dev/use-cases/better-auth/overview) for wiring `identifyUser` alongside the audit pipeline. Then either (a) partition the audit dataset by `audit.context.tenantId`, or (b) one drain per tenant if hard isolation is required. Never query audits without a tenant filter. ### 4. Retention Pick a window per drain and document it. Enforce at the drain layer, not in app code, because the drain already has audited mechanisms for it (lifecycle policies, `DELETE` jobs, dataset retention). | Framework | Typical retention | | --------- | ------------------------------------------------------------------- | | SOC2 | 1 year minimum, 7 years recommended | | HIPAA | 6 years | | PCI DSS | 1 year (3 months immediately accessible) | | GDPR | "As long as necessary" — see "GDPR vs append-only" below | How to enforce per drain: - **FS**: `createFsDrain({ maxFiles })` + a daily compactor. - **Postgres**: `DELETE FROM audit_events WHERE timestamp < now() - interval '7 years'` on a cron. - **Axiom / Datadog / Loki**: dataset-level retention policy. ### 5. GDPR vs append-only The right to be forgotten collides with audit immutability. Recommended pattern: 1. Keep audit rows immutable and chain-verified. 2. Encrypt PII fields (email, name, IP) with a per-actor key held outside the audit store. 3. To "forget" a user, delete their key. The audit row stays, the chain stays valid, the PII becomes unreadable (crypto-shredding). A built-in `cryptoShredding` helper is on the roadmap; until then, encrypt in a custom enricher. ## Step-by-step buildout ### Step 1: Wire the pipeline (one-time) The wiring shape is the same in every framework: register `auditEnricher()` so `event.audit.context` gets `requestId`, `traceId`, `ip`, `userAgent`, and (if configured) `tenantId` automatically, then add a main drain plus an audit-only drain. The minimal Nuxt / Nitro setup looks like this: ```ts // server/plugins/evlog.ts import { auditEnricher, auditOnly, signed } from 'evlog' import { createAxiomDrain } from 'evlog/axiom' import { createFsDrain } from 'evlog/fs' export default defineNitroPlugin((nitroApp) => { const auditSink = auditOnly( signed(createFsDrain({ dir: '.audit/' }), { strategy: 'hash-chain' }), { await: true }, ) const main = createAxiomDrain({ dataset: 'logs' }) nitroApp.hooks.hook('evlog:enrich', auditEnricher({ tenantId: ctx => ctx.headers?.['x-tenant-id'], })) nitroApp.hooks.hook('evlog:drain', async (ctx) => { await Promise.all([main(ctx), auditSink(ctx)]) }) }) ``` For Hono, Express, Next.js, or standalone scripts / workers, see [`references/framework-wiring.md`](references/framework-wiring.md). The pattern is identical. Only the framework integration helper changes. ### Step 2: Define the action vocabulary Audits get queried and alerted on by `audit.action`. A typo is a missing alert, so centralise the list. For a bounded context with several actions, prefer a catalog: one prefix, typed keys, autocomplete on the wire format `${prefix}.${KEY}`: ```ts // app/audit/billing.ts import { defineAuditCatalog } from 'evlog' export const billingAudit = defineAuditCatalog('billing', { INVOICE_REFUND: { target: 'invoice' }, PLAN_CHANGE: { target: 'subscription' }, }) ``` At the call site: `log.audit(billingAudit.INVOICE_REFUND({ actor, target, outcome: 'success' }))`. Add the opt-in `declare module 'evlog' { interface RegisteredAuditCatalogs { billing: typeof billingAudit } }` augmentation to get autocomplete everywhere. For one-off actions that don't fit a catalog, `defineAuditAction` still works: ```ts // app/audit/actions.ts import { defineAuditAction } from 'evlog' export const InvoiceRefund = defineAuditAction('invoice.refund', { target: 'invoice' }) export const ApiKeyRevoke = defineAuditAction('apiKey.revoke', { target: 'apiKey' }) ``` Catalog conventions and scaling recipes (folder per domain, npm packages per bounded context): https://www.evlog.dev/learn/catalogs Naming conventions: - `noun.verb` (`invoice.refund`, not `refundInvoice`). - Past tense if the audit is logged after the fact (`invoice.refunded`); present tense when wrapped by `withAudit()` (which resolves the outcome itself). - Lowercase, dot-delimited, no spaces: for hand-written action ids (`defineAuditAction`, inline `log.audit`). Catalog entries follow the catalog convention instead: UPPER_SNAKE_CASE keys under a lowercase prefix, producing wire actions like `billing.INVOICE_REFUND`. That is intentional. Do not lowercase the keys. ### Step 3: Instrument call sites
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen