Skip to main content
Docs navigation

Audit & SIEM

Everything that matters leaves a record: every dispatch and its outcome, every approval decision, every policy edit, every credential minted or revoked, every sign-in — even the failed ones.

emisar console · Audit log
Every dispatched action with its actor, target runner, outcome, source IP, and time — filterable, and exportable to your SIEM.
siem
$ curl -sN -H "Authorization: Bearer $AUDIT_KEY" \
    https://emisar.dev/api/audit?since=2026-07-23T00:00:00Z
{"id":"019f8a5f-7759-7a33-a47e-9a7c28e10bd1","request_id":"req_cF-s3B3Z-Iu38MDhBCdqlw","account_id":"019f8a5f-74c7-7b9a-980a-0e00422d2541","actor_kind":"user","actor_label":null,"actor_id":"019f8a5f-749b-72b9-b70f-04b56ebf9e83","ip_address":null,"event_type":"action_run.pending_approval","occurred_at":"2026-07-22T15:09:00.121466Z","target_id":"019f8a5f-75b2-7a64-a955-152da6a993f3","mcp_client_metadata":null,"user_agent":null,"payload":{"action":"postgres.reload_conf","executed_command_truncated":false,"run_id":"019f8a5f-7758-7cfc-8790-77c9aa9400fd"},"target_label":"pg-primary-iad","target_kind":"runner"}
One event per line, forward-only
Every line is a complete JSON object — the event_type, the actor_id / target_id to correlate on, the request metadata (ip_address, request_id), and the full payload.

What gets recorded#

Events are named domain.actionaction_run.denied (a dispatch policy rejected, with the reason), approval.approved, policy.updated, membership.role_changed, user.sessions_revoked, … over sixty types across runs, approvals, policies, packs, runners, credentials, team, and account changes. Each carries the actor (operator, API key, or runner), the affected entity, a structured payload (a policy edit records its exact diff), and an account-scoped timestamp. Audit rows are written in the same database transaction as the change they describe — an action that committed has its audit row, always.

Reading it in the dashboard#

Audit filters by event type or actor kind; every row expands to the full payload, and references resolve to live labels (the runner's name, the user's email) so you're not cross-referencing UUIDs. LLM-driven runs carry their attribution — which client, which key, which session — plus the required reason, so you can answer what the agent did last night, and why, with a single filter.

Exporting the trail#

Two ways out: the Export CSV button on the Audit log downloads exactly the view you've filtered to, and the export endpoint serves NDJSON — one event per line, cursor-paginated, forward-only. Mint a read-only audit-export token from the Audit page; that token can read events and nothing else — it's a separate credential kind from an MCP key, so it can never list runners or execute an action. Only available on Team & Enterprise.

shell
# first pull — everything since a timestamp
$ curl -s "https://emisar.dev/api/audit?since=2026-06-01T00:00:00Z&limit=500" \
    -H "Authorization: Bearer $AUDIT_KEY"

# follow-ups — pass the cursor from the previous response
$ curl -s "https://emisar.dev/api/audit?cursor=$NEXT" \
    -H "Authorization: Bearer $AUDIT_KEY"
  • X-Next-Cursor is your resume point — the last row on the page — set whenever a page returns events, so your next poll starts strictly after what you already have and a short page is never re-read. The RFC 5988 Link: <…>; rel="next" header is the separate "there's more right now" signal and rides only a full page — Splunk-style collectors follow it to keep paging, Datadog-style ones just track the X-header. No Link means you're caught up: poll again later from the last X-Next-Cursor you saw (an empty page sets neither header, so you hold your place).
  • Pages are capped at 1,000 events; ordering is stable (occurred_at, then id), so a poller never misses or double-counts an event.
  • Retention follows your plan — 7 days on Free, 90 on Team, 365 on Enterprise. Ship to your SIEM ahead of the window and retention becomes your SIEM's policy, not ours. Each event's horizon is fixed when it's written, so a plan downgrade only shortens the window for new events — it never retroactively deletes history — and both the plan change and every retention prune are themselves recorded in the log.

The runner-side journal#

Independently of the cloud, every runner writes a JSONL line per action attempt to /var/log/emisar/events.jsonl — argv hashes, exit codes, redaction counts, output digests — each line chained to the previous by SHA-256. emisar audit verify --all walks the chain (including rotated files) and flags a break. Treat it as tamper-evident, not tamper-proof: root on the host can truncate the tail or rewrite lines and re-chain them so verify still passes. The record that can't be forged from the host is the cloud audit — it's off-host and append-only. So ship the host journal through your log pipeline and stream the cloud audit to your SIEM, then cross-reference them: an action in one record but not the other is the tamper signal, and hiding an action means forging both consistently — with one of them out of reach on the host.

Worth alerting on.
user.mfa_reset_by_admin on an account you didn't expect, policy.updated (the rules changed), pack-trust transitions (new bytes were approved), and on the runner side validation_failed / action_blocked_by_admission — an agent repeatedly asking for things it can't have is the most interesting signal you'll get.