Audit export to SIEM
Epic 6714. Child C7 of the architecture. Covers PRD ask F3 and its S.1 gate. `depends_on` the retention child only to serialize schema migrations.
## Original requirement (epic body, verbatim)
"Missing features: ... audit export to SIEM ..." Done means: "Every feature above runs." Decision D-F3: one destination type, HTTPS JSON.
## Acceptance criteria (PRD, verbatim)
**Environment:** dev = `dev.jaah.ai` (browser face `https://app.dev.jaah.ai`); browser user flows; unit tests required but never sufficient; ON by default on dev; the verifying org is on Enterprise.
- **F3.1** An org admin on a plan with the SIEM switch configures an HTTPS destination (URL plus an auth header or HMAC secret, stored as a secret). Every org audit/activity event is delivered as JSON within 5 minutes, at-least-once with retry. A "send test event" button and the last delivery status and error are shown.
- **S.1** Each of F1–F8 has its own plan switch, per-org overridable, default OFF. With the switch off, the feature's settings show an upgrade prompt and its APIs return 403 naming the plan feature.
- **Quality** Each new setting appears on the page where peers live. Labels follow `docs/standards/ui-copy-standards.md`. Nothing truncated or overflowing. Every refusal names what to do next. A cramped, unlabeled or raw-key UI fails.
## Approach (architecture C7, settled)
- Schema: `org_siem_destinations` (org_id unique, url https only, `auth_mode` header|hmac, `header_name`, `secret` sealed via `credential_crypto.seal_secret` with AAD = row id, `cursor_event_id bigint`, `last_attempt_at`, `last_success_at`, `last_status`, `last_error`, `enabled`).
- Delivery: system cron `siem-export` (60 s): per destination read `activity_events WHERE org_id=… AND id > cursor ORDER BY id LIMIT 500`, POST a JSON array (id, org_id, created_at, kind, action_type, actor, summary, path, method, status, data), HMAC `X-Jaah-Signature: sha256=…` or the header; 2xx → advance cursor; else record error and retry next cycle (consumers dedupe on `id`). Delivers only while `siem_export_enabled` is on.
- Scope: `activity_events` only (not `CommandAudit`). "Send test event" posts one synthetic event without moving the cursor. An outage longer than the org's retention loses those events, shown as an error (R8, accepted).
- Gate: `require_switch(..., "siem_export_enabled")`; `PlanUpgradeNotice` when off. Console: Settings › Security › Audit export.
- One expand-only migration; re-point `down_revision` at rebase.
Credentials & reach:
- Console oracles on dev: `jaah/debug/main/operator-login` on `app.dev.jaah.ai` — verified.
- F3 SIEM capture: `webhook.site` request capture, no credential — verified — answered 200.
Out of scope: Do NOT include `CommandAudit`, syslog or S3 destinations (deferred, D-F3). Do NOT change the operator's S3 security archive (N5). Do NOT touch the marketing site or `docs/compliance/public/` (N2). Never show a stored secret after save.
Done means:
- F3.1 → verify: on dev, point the export at a request-capture endpoint, create a project and observe the event arrive within 5 minutes (HMAC signature verifiable). Click "Send test event" and observe it arrive. Break the URL and observe the error and retry status in the console; fix it and observe the backlog delivered.
- Non-HTTPS URL refused with a message (unit test plus console).
- S.1 → verify: on dev, view the audit export settings on a Business org (upgrade prompt; API 403 naming "SIEM export") and on an Enterprise org (available).
- Quality → verify: Settings › Security › Audit export at desktop and phone width: readable labels, status/error legible and not truncated, no raw keys.
Outcome
SIEM audit export shipped (PR #5986, lane-merged 1b892842b). Includes the table, the 60 s cron, the API and an Audit export tab beside the IP allowlist. Review blockers were fixed and the local visual check was good. Full gate red only on develop-side tests (draft #7391). Dev oracles deferred. Deploy: dev apply OK; bundle upload stalled, superseded.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)