# Compliance-Audit-Export Phase 10 Task #67. Tamper-evident, SOC2/HIPAA/GDPR-ready audit log with a self-service export UI for tenants and a system-wide export for platform admins. ## Overview `AuditEvent` (`prisma/schema/core.prisma`) is the single normative store for privileged actions across the platform. Phase 10 extended the model with: | Field | Purpose | | ----------------- | ---------------------------------------------------------- | | `actorEmail` | denormalized actor email for compliance reports | | `actorIp` | first-hop client IP (cf-connecting-ip / x-real-ip / xff) | | `actorUserAgent` | UA capped at 512 chars | | `sourceModule` | `auth` / `billing` / `admin` / `plugin:foo` | | `outcome` | `success` / `failure` / `blocked` | | `dataSensitivity` | `public` / `internal` / `confidential` / `pii` | | `regulatoryScope` | string[]: `gdpr` `hipaa` `soc2` `pci` | | `previousHash` | predecessor `eventHash` for the same tenant chain | | `eventHash` | HMAC-SHA-256 over canonical payload + previousHash | Existing fields (`tenantId`, `actorId`, `actorType`, `action`, `targetType`, `targetId`, `requestId`, `metadata`) remain unchanged. ## Hash-Chain Per-tenant chain: each tenant's events form an independent linked-list keyed by `previousHash`. Platform-level events (tenantId IS NULL) form their own chain. ``` eventHash_n = HMAC-SHA-256(AUDIT_CHAIN_SECRET, eventHash_{n-1} || canonicalJSON(payload_n)) ``` `canonicalJSON` is the JSON encoding produced by `canonicalizeAuditPayload` in `packages/db/src/audit-hash.ts`: object keys sorted recursively, `undefined` dropped, arrays preserve order. Without `AUDIT_CHAIN_SECRET` the helper falls back to plain SHA-256 — still tamper-evident for read-only attackers but trivially forgeable. `verifyAuditChain(events, { secret })` walks the chain oldest → newest and returns the first break with one of: - `previous-hash-mismatch` — chain discontinuity (an event was inserted or deleted) - `hash-mismatch` — payload was modified in-place ## Auto-Capture ### API (Hono) `apps/api/src/middleware/audit-context.ts` registers an AsyncLocalStorage binding on every request and exposes `getAuditContext()`. Deeper code (`createAuditEvent` calls in services) automatically picks up actor IP, user agent, request id without threading the request object. ```ts import { createAuditContextMiddleware } from "./middleware/audit-context.js"; app.use("*", createAuditContextMiddleware()); ``` ### Web (React-Router v7) `apps/web/app/utils/audit/withAuditContext.ts` mirrors the API helper for loaders/actions: ```ts export const action = async (args: ActionFunctionArgs) => withAuditContext(args, async () => { // ...createAuditEvent calls deeper in the stack pick up actor ctx }); ``` For ad-hoc reads `extractWebActorIp(request)` and `extractWebActorUserAgent(request)` are exported too. ## Export UI Two routes ship: - `/admin/audit-export` — platform admin, system-wide. Optional `tenantId` filter. - `/app/$tenant/settings/audit-export` — tenant self-service. tenantId is force-applied server-side; query-string spoofing is silently dropped. Both stream a CSV up to 50 000 rows (`AUDIT_EXPORT_MAX_ROWS`). The **first line** of the CSV is the `verifyAuditChain` integrity header so the download itself is the auditor evidence package: ``` # verifyAuditChain: ok=true inspected=12345 firstBreakId=- reason=- id,createdAt,tenantId,actorType,actorId,… ``` The HTTP response also carries an `X-Audit-Chain-Status: ok|broken` header so a programmatic client can branch without parsing the CSV. Filters: - `from` / `to` (ISO date, inclusive UTC range; max 1 year recommended) - `regulatoryScope` (multi) - `actorType` (`user` / `admin` / `api_key` / `system` / `plugin`) - `outcome` (`success` / `failure` / `blocked`) - `tenantId` (admin variant only) CSV cells with commas, quotes or newlines are RFC-4180 escaped; arrays join with `|`; JSON metadata is stringified. ## Retention `apps/worker/src/jobs/audit-retention.ts` exposes `runAuditRetentionJob` which the worker host wires to a daily cron. Plan-based defaults (`resolveAuditRetentionPolicy`): | Regulatory scope on tenant events | Retention | | --------------------------------- | --------- | | `soc2` or `hipaa` | 7 years | | `gdpr` | 6 years | | none | 1 year | Every non-empty sweep emits a meta-audit (`audit.retention.swept`) so the cleanup itself is traceable. ## Operator Procedures ### Bootstrap 1. Generate a secret: `openssl rand -base64 32`. 2. Set `AUDIT_CHAIN_SECRET=` in Coolify (platform-wide, never per-tenant). 3. Apply the Prisma migration `20260517180000_10_audit_event_compliance`. ### Self-service export (tenant admin) 1. Navigate to `Settings → Audit Export`. 2. Pick date range + filters. 3. Click *Download CSV*. 4. Verify the first line shows `ok=true`. If `ok=false`, contact platform support — chain integrity has been compromised. ### Forensic verify (platform admin) ``` # In a node REPL with the export CSV: const { verifyAuditChain } = await import("@toolctrl/db/audit-hash"); // load events from CSV/DB, then: verifyAuditChain(events, { secret: process.env.AUDIT_CHAIN_SECRET }); ``` ### Tampering detection drill ``` psql ... -c "UPDATE \"AuditEvent\" SET metadata = '{\"tampered\":true}' WHERE id = ''" ``` Re-export the tenant — the integrity header now reports `ok=false reason=hash-mismatch firstBreakId=`. ## Roadmap - Admin per-tenant retention-policy override JSON - S3-glacier auto-archive before retention delete - Pre-built SOC2 / GDPR / HIPAA report templates (PDF with signature hash) - CLI: `pnpm audit:verify-chain --tenant ` direct DB walker - Outbox-driven audit writes for transactional consistency with business writes (Phase 03 #42 integration) ## Related - `prisma/migrations/20260517180000_10_audit_event_compliance/` - `packages/db/src/audit-hash.ts` — chain helpers - `packages/db/src/audit-events.server.ts` — single write-path - `apps/api/src/middleware/audit-context.ts` — API auto-capture - `apps/web/app/utils/audit/withAuditContext.ts` — web auto-capture - `apps/web/app/routes/admin/audit-export.tsx` — admin export UI - `apps/web/app/routes/app.$tenant/settings/audit-export.tsx` — tenant export UI - `apps/worker/src/jobs/audit-retention.ts` — retention worker