187 lines
6.5 KiB
Markdown
187 lines
6.5 KiB
Markdown
# 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=<value>` 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 = '<id>'"
|
|
```
|
|
|
|
Re-export the tenant — the integrity header now reports
|
|
`ok=false reason=hash-mismatch firstBreakId=<id>`.
|
|
|
|
## 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 <id>` 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
|