Docs / Audit & SIEM

The audit trail

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. This page covers reading the trail in the dashboard, streaming it to your SIEM, and how the runner-side journal complements it.

What gets recorded

Events are named domain.actionpolicy.evaluated (one per dispatch, with the decision), approval.approved, policy.updated, membership.role_changed, user.sign_in_failed, … about fifty 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 in the sidebar: filter 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 "what did the agent do last night, and why" is a filter, not an investigation.

Streaming to a SIEM

The export endpoint serves NDJSON — one event per line, cursor-paginated, forward-only. Mint a key with the audit:read scope from the Audit page; that key can read events and nothing else — it can never list runners or execute an action.

# 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"
  • The next cursor arrives both as an RFC 5988 Link: <…>; rel="next" header and a plain X-Next-Cursor header — Splunk-style collectors follow the Link, Datadog-style ones read the X-header. No headers on the last page means you're caught up; poll again later with the same cursor.
  • 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.

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 proves the chain (including rotated files). The cloud is the searchable system of record; the journal is the host-side forensics copy that doesn't depend on us — cutting or editing a line breaks the chain detectably, though a root attacker can of course delete the whole file. Ship it with your normal log pipeline if you want both records off-host.

Worth alerting on. user.sign_in_failed spikes (someone probing your team), 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.