Audit Logging
Honeyframe records security-relevant events to a single append-only table. The audit log is the canonical record of "who did what when" — administrators view it through the platform UI, and operators can forward it to a SIEM via direct database access.
Storage
The log lives in the dataintel.audit_log table. The schema:
| Column | Type | Notes |
|---|---|---|
log_id | bigint | Primary key, auto-increment. |
user_id | int / null | FK to dataintel.users. Null for system or unauthenticated events. |
action | text | Free-form action string (login, chat_query, password_change, ...). |
resource_type | text / null | Object kind (dashboard, dataset, user, ...). |
resource_id | text / null | Object identifier; free-form because target keyspaces differ. |
details | jsonb / null | Structured payload — query parameters, before/after values, error details. |
ip_address | text / null | Client IP captured from X-Forwarded-For (or socket peer if no proxy). |
created_at | timestamptz | Server time at insert. |
The table is append-only by convention — there is no application code that updates or deletes rows. To enforce that, revoke UPDATE/DELETE from the application role and grant only INSERT/SELECT. The application never reads the log on the request path; the only readers are the audit viewer endpoints.
What is recorded
The log_audit() helper at paas/backend/middleware/audit_middleware.py is the single insert path. Any router that calls it contributes to the log. As of v0.0.x, the wired event sources are:
| Source | Action strings |
|---|---|
routers/auth.py | login, google_login, password_change, password_reset |
routers/chat.py | chat_query (with prompt + agent metadata in details) |
This is intentionally narrow — additional events are added one router at a time. There is no automatic CRUD audit middleware that records every mutation; events are recorded only where a log_audit(...) call has been added explicitly. If you need broader coverage, the migration path is:
- Add
await log_audit(db, user["user_id"], "<action>", resource_type="...", resource_id="...", details={...}, ip_address=request.client.host)after each successful mutation. - Use the action conventions documented under Audit conventions below.
Reading the log
Administrators read the log through /api/audit:
| Endpoint | Description |
|---|---|
GET /api/audit?action=...&user_id=...&limit=50&offset=0 | Filtered, paginated list of log rows. Returns {total, logs}. |
GET /api/audit/actions | Distinct action strings with row counts — useful for building a filter dropdown. |
GET /api/audit/summary | Aggregate summary (counts by user, by action, by day). |
All three require role admin. There is no per-row authorization; admins see every event in the org.
In the UI, the audit viewer is reached from the Settings → Audit Log page. The same filters are available there.
Audit conventions
When adding new audit calls, follow these conventions so log analysis stays consistent:
- Action strings are
<verb>or<resource>_<verb>in lowercase snake_case:login,dashboard_create,dataset_delete,permission_grant. Prefer verbs in present tense (login, notlogged_in). resource_typematches the resource convention from the Permissions Reference:dashboard,dataset,recipe,flow,connection,group,org.resource_idis the canonical identifier (numeric ID as string, slug, etc.). Always serialize to text — the column istext, notbigint.detailsis JSONB. Keep it small (< 4 KB) and structured. Don't dump entire request bodies. Useful payloads:{"old_role": "viewer", "new_role": "editor"},{"query_size": 1024, "duration_ms": 87}.ip_address— passrequest.client.hostfrom the FastAPIRequest. Behind a reverse proxy, ensureset_real_ip_fromis configured so this reflects the client IP, not the proxy IP.- Failure events should be logged too. Pattern: log the attempt with
details={"error": "..."}even when the operation fails. Without failure logs, the audit trail has no record of denied actions.
SIEM forwarding
There is no built-in SIEM forwarder. The recommended pattern is a periodic export from the database to your SIEM's ingestion pipe:
# Example: append last 5 minutes of events to a JSON file the SIEM tails
psql "$AUDIT_DSN" -At -c "
SELECT json_build_object(
'log_id', log_id,
'user_id', user_id,
'action', action,
'resource_type', resource_type,
'resource_id', resource_id,
'details', details,
'ip_address', ip_address,
'created_at', created_at
)
FROM dataintel.audit_log
WHERE created_at > now() - interval '5 minutes'
ORDER BY created_at
" >> /var/log/honeyframe/audit.jsonl
Run this from a systemd.timer every 1–5 minutes. For high-volume installs, replace the polling with a logical-replication slot or PostgreSQL LISTEN/NOTIFY trigger.
Retention
Honeyframe does not delete audit rows. The default retention is "forever". To enforce a retention window, schedule a DELETE FROM dataintel.audit_log WHERE created_at < now() - interval 'N days' job. Make sure your SIEM has ingested the rows before deletion.
Threat model
The audit log is for post-incident review, not real-time alerting. Inserts are best-effort within the request — if the audit insert fails, the request still succeeds (the failure is logged to stderr, not retried). For real-time alerts on critical events, hook into the database via logical replication or triggers; do not rely on the application's audit-call path being reachable during an incident.