Files
HiveGrid-Wireframes/hivegrid-cloud/docs/features/compliance-audit.md
T

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