Admin Audit API - ericfitz/tmi GitHub Wiki
Admin Audit Query API
TMI exposes four read-only endpoints under /admin/audit/ for investigators to query the two audit streams: the per-resource audit_entries trail (threat-model object changes) and the system_audit_entries trail (admin-write evidence). Both require the admin role; service-account tokens are categorically denied (see Authentication#privilege-restrictions).
Overview
| Operation | Path | Description |
|---|---|---|
GET /admin/audit/system |
listSystemAuditEntries |
List admin-write events with filters and cursor pagination |
GET /admin/audit/system/{entry_id} |
getSystemAuditEntry |
Get a single system audit entry |
GET /admin/audit/threat_models |
listAdminThreatModelAuditEntries |
Cross-TM audit trail with filters and cursor pagination |
GET /admin/audit/threat_models/{entry_id} |
getAdminThreatModelAuditEntry |
Get a single threat-model audit entry |
All four endpoints require an interactive JWT with the admin role. No step-up re-authentication is required on reads.
Pagination
Both list endpoints use keyset cursor pagination rather than limit/offset. Audit tables are append-only streams; offset paging suffers from page-drift duplicates on active tables and degrades at depth. The cursor encodes the (created_at, id) of the last row returned and is opaque to callers.
Response envelope:
{
"entries": [...],
"total": 1284,
"limit": 50,
"next_cursor": "eyJ0IjoiMjAyNi0wNi0xMVQxNDozMDowMFoiLCJpIjoiN2ZhODVmNjQtNTcxNy00NTYyLWIzZmMtMmM5NjNmNjZhZmE2In0"
}
total: count of all filter-matching rows (computed per request).next_cursor: present only when a full page was returned; absent when the last page is reached.limit: 1–100, default 50.
Iterating all pages
TOKEN="<your-admin-jwt>"
CURSOR=""
while true; do
if [ -z "$CURSOR" ]; then
RESULT=$(curl -s "https://api.tmi.dev/admin/audit/system?limit=100" \
-H "Authorization: Bearer $TOKEN")
else
RESULT=$(curl -s "https://api.tmi.dev/admin/audit/system?limit=100&cursor=$CURSOR" \
-H "Authorization: Bearer $TOKEN")
fi
echo "$RESULT" | jq '.entries[]'
CURSOR=$(echo "$RESULT" | jq -r '.next_cursor // empty')
[ -z "$CURSOR" ] && break
done
An invalid cursor value returns 400 Bad Request. An empty result set (all filters match nothing) returns an empty entries array with total: 0 and no next_cursor.
GET /admin/audit/system
Lists system_audit_entries — one row per successful /admin/* write.
Query parameters
| Parameter | Type | Description |
|---|---|---|
actor_email |
string | Filter by actor email address |
actor_provider |
string | Filter by identity provider (google, github, tmi, …) |
created_after |
ISO8601 | Return entries created after this timestamp |
created_before |
ISO8601 | Return entries created before this timestamp |
http_method |
string | Filter by HTTP method (PUT, POST, DELETE, PATCH) |
path_prefix |
string | http_path LIKE '{value}%' — unindexed, use alongside other filters |
field_path |
string | Exact match on the field path (e.g., auth.step_up_window_seconds) |
limit |
int | Page size 1–100, default 50 |
cursor |
string | Opaque continuation cursor from previous response |
All filters are optional and AND-combined.
Response fields
Each entry in the entries array:
| Field | Description |
|---|---|
id |
UUID of the system audit entry |
actor.email |
Actor's email address |
actor.provider |
Actor's identity provider |
actor.provider_id |
Actor's provider-scoped user ID |
actor.display_name |
Actor's display name at time of write |
http_method |
HTTP method used |
http_path |
Full request path |
field_path |
Setting key or field path changed (if applicable) |
old_value_redacted |
Previous value, redacted to safe summary |
new_value_redacted |
New value, redacted to safe summary |
change_summary |
Human-readable description of the change |
created_at |
RFC3339Nano timestamp |
Example: find all admin writes by a specific actor in the last 7 days
curl "https://api.tmi.dev/admin/audit/system?actor_email=charlie%40example.com&created_after=2026-06-04T00:00:00Z" \
-H "Authorization: Bearer $TOKEN" | jq '.entries[] | {path: .http_path, method: .http_method, when: .created_at}'
GET /admin/audit/system/{entry_id}
Returns a single system_audit_entries row by its UUID.
404if the entry does not exist (or the UUID is not a valid format →400).
The entry_id in system_audit.admin_write webhook payloads links directly to this endpoint.
GET /admin/audit/threat_models
Lists audit_entries across all threat models — investigator view with no fixed threat_model_id constraint.
Query parameters
| Parameter | Type | Description |
|---|---|---|
actor_email |
string | Filter by actor email |
actor_provider |
string | Filter by identity provider |
created_after |
ISO8601 | Return entries after this timestamp |
created_before |
ISO8601 | Return entries before this timestamp |
change_type |
enum | One of: created, updated, patched, deleted, restored, rolled_back |
object_type |
enum | One of: threat_model, diagram, threat, asset, document, note, repository |
threat_model_id |
UUID | Restrict to a single threat model |
limit |
int | Page size 1–100, default 50 |
cursor |
string | Opaque continuation cursor |
Example: find all deletions this week
curl "https://api.tmi.dev/admin/audit/threat_models?change_type=deleted&created_after=2026-06-08T00:00:00Z" \
-H "Authorization: Bearer $TOKEN" | jq '.entries[] | {who: .actor_email, what: .object_type, id: .object_id, when: .created_at}'
GET /admin/audit/threat_models/{entry_id}
Returns a single audit_entries row by UUID. The entry already carries threat_model_id, so no parent path segment is needed.
404if the entry does not exist;400for an invalid UUID format.
Error responses
| Status | Condition |
|---|---|
400 |
Invalid cursor, invalid UUID format, or invalid enum value |
401 |
Missing or expired JWT |
403 |
Non-admin JWT, or service-account (CCG) token |
404 |
Single-entry lookup: entry not found |
429 |
Rate limit exceeded |
Related pages
- Authentication#privilege-restrictions -- Why CCG tokens are denied on all /admin/* routes
- Maintenance-Tasks#audit-retention-and-immutability -- Retention and immutability of the underlying tables
- Webhook-Integration#operator-pinned-alert-sink -- Real-time alert sink for system audit events
- REST-API-Reference -- Full OpenAPI-generated endpoint reference