Skip to main content

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:

ColumnTypeNotes
log_idbigintPrimary key, auto-increment.
user_idint / nullFK to dataintel.users. Null for system or unauthenticated events.
actiontextFree-form action string (login, chat_query, password_change, ...).
resource_typetext / nullObject kind (dashboard, dataset, user, ...).
resource_idtext / nullObject identifier; free-form because target keyspaces differ.
detailsjsonb / nullStructured payload — query parameters, before/after values, error details.
ip_addresstext / nullClient IP captured from X-Forwarded-For (or socket peer if no proxy).
created_attimestamptzServer 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:

SourceAction strings
routers/auth.pylogin, google_login, password_change, password_reset
routers/chat.pychat_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:

  1. Add await log_audit(db, user["user_id"], "<action>", resource_type="...", resource_id="...", details={...}, ip_address=request.client.host) after each successful mutation.
  2. Use the action conventions documented under Audit conventions below.

Reading the log

Administrators read the log through /api/audit:

EndpointDescription
GET /api/audit?action=...&user_id=...&limit=50&offset=0Filtered, paginated list of log rows. Returns {total, logs}.
GET /api/audit/actionsDistinct action strings with row counts — useful for building a filter dropdown.
GET /api/audit/summaryAggregate 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, not logged_in).
  • resource_type matches the resource convention from the Permissions Reference: dashboard, dataset, recipe, flow, connection, group, org.
  • resource_id is the canonical identifier (numeric ID as string, slug, etc.). Always serialize to text — the column is text, not bigint.
  • details is 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 — pass request.client.host from the FastAPI Request. Behind a reverse proxy, ensure set_real_ip_from is 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.