grithdocs

Audit

Page the audit log, aggregate a window, export it, and read the exfiltration rollup.

The same hash-chained records grith audit prints. Every route here is a sensitive read: send the dashboard token as x-grith-csrf once one exists.

MethodPathWhat
GET/api/auditA page of records
GET/api/audit/summaryCounts for one window (new in 0.3.1)
GET/api/audit/{id}One record
GET/api/audit/exportBulk export, JSON or CSV
GET/api/audit/exfil-statsExfiltration rollup over the last 500 records

Every one of them verifies the chain first. A tampered chain is 500 AUDIT_CHAIN_BROKEN, a mismatched archive boundary is 500 AUDIT_CHAIN_ANCHOR_MISMATCH, and a missing anchor is 503 AUDIT_CHAIN_UNANCHORED. None of them repairs anything - run grith audit diagnose, which still works when the rest of the surface does not.

GET /api/audit

ParamDefaultNotes
limit20Capped at 100
offset0
session_id-A UUID. Returns every record for that session and ignores paging
includefullall also returns the compact short-circuit rows

The response is {records, total, limit, offset, include} - or just {records, total} for a session_id query, which has no paging to report. A record carries id, timestamp, session_id, plugin_id, tool_call_type, arguments_summary, arguments_hash, composite_score, proxy_action (allow, queue or deny), filter_results, evaluation_time_ms, source and, for supervised calls, supervised_tool, supervised_pid and project_name.

arguments_summary is truncated and redacted; arguments_hash is the SHA-256 of the full arguments, which is what makes the chain verifiable without storing the arguments.

GET /api/audit/summary

since 0.3.1 One query, one window, one consistent set of numbers. The dashboard used to build its breakdown client-side from whatever page it happened to have while the headline came from a whole-database count, so the two described different populations.

GET /api/audit/summary?window=7d
{
"window": "7d",
"since": "2026-08-17T09:14:23+00:00",
"total": 4812,
"allow": 4702,
"queue": 96,
"deny": 14,
"include": "full"
}

window is today, 7d, 30d or all, defaulting to 7d. An unrecognised value falls back to 7d rather than returning 400, so a stale browser tab still renders. today is midnight UTC, matching how the timestamps are stored - not the browser's local midnight. since is null for all. include behaves as it does on /api/audit.

GET /api/audit/export

ParamDefaultNotes
formatjsoncsv returns text/csv; charset=utf-8
limit1000
include_coldfalseAlso read the compressed archives under <audit_dir>/cold/
from_date-YYYY-MM-DD. Only filters which archive files are read, not active records

JSON is {records, count}. With include_cold=true the archive rows are merged in and the whole set is re-sorted newest-first; leave it off and the export stays cheap.

GET /api/audit/exfil-stats

A rollup over the last 500 records, counting only the queued and denied ones where an egress, DLP, canary or containment filter matched:

GET /api/audit/exfil-stats
{
"total_blocked": 6,
"total_queued": 11,
"total_redacted": 2,
"by_protocol": { "http": 9, "network": 5, "shell-transport": 3 },
"top_blocked_destinations": [ { "domain": "sink.example", "count": 4 } ]
}

Destinations are parsed best-effort out of arguments_summary, so a call whose summary carries no URL contributes to the protocol counts but not to the destination list. The list is the top 10.

See also

Last updated: 2026-08-24Edit this page on GitHub →