Skip to main content

Audit log

Every administrative mutation that flows through HomeCore's REST API is recorded in a tamper-evident audit log: who acted, what they changed, when, and from where. The log is the foundation for security reviews, compliance audits, and post-incident analysis.

What gets recorded

The recorder fires on every authenticated mutation: rule create / update / delete, scene activations, mode toggles, plugin restart / deregister, user CRUD, role changes, dashboard edits, scope changes, backup / restore, and auth events (login success / failure, refresh, logout).

Reads are not audited by default — the volume would dwarf the actionable events. The recorder is designed to be safe: failures inside record_audit are logged as warnings but do not abort the originating operation, so a transient SQLite hiccup never blocks a legitimate mutation.

Storage

The log lives in its own SQLite database, separate from the device state and history DBs:

data/audit.db          # default, sibling of state.redb / history.db

A separate file means audit retention can be tuned independently and backup tooling can include or exclude it as policy requires (the built-in POST /system/backup bundles all three).

A background task runs every six hours and prunes entries older than [auth].audit_retention_days (default 365). Set the value to 0 to disable pruning entirely.

Entry shape

Each row carries:

FieldDescription
idAuto-incrementing row id
tsUTC timestamp of the action
actor_typeOne of user, api_key, local_admin, ip_whitelist, system, anonymous
actor_idUUID of the user / API key (when applicable)
actor_labelHuman-readable identifier (username, key name, or IP)
event_typeSnake-case event name, e.g. rule.created, auth.login, plugin.restarted
scope_usedThe JWT scope that authorised the action
target_kind / target_idWhat was acted upon (e.g. kind="rule", id="<uuid>")
correlation_idLinks related events from a single request chain
ip / user_agentWhere the request came from
resultsuccess, failure, or denied
detailFree-form JSON for context (changed fields, error messages)

actor_type distinguishes how the request was authenticated:

  • user — JWT issued via /auth/login
  • api_keyAuthorization: Bearer hc_sk_...
  • local_admin — Connected over the admin UDS
  • ip_whitelist — Bypassed JWT via the deprecated CIDR whitelist
  • system — The recorder itself, e.g. background prune jobs
  • anonymous — Pre-auth attempts (failed logins land here)

Querying

REST

GET /api/v1/audit returns recent entries newest-first. Requires the audit:read scope (granted to the Admin, Observer, and ServiceOperator preset roles).

Query parameters (all optional):

ParamDescription
actor_idFilter by actor UUID
actor_typeuser, api_key, local_admin, ip_whitelist, system, anonymous
event_typeExact event name
target_kindrule, scene, plugin, user, etc.
target_idExact target id
resultsuccess, failure, denied
fromRFC3339 lower bound (inclusive)
toRFC3339 upper bound (inclusive)
limitDefault 100
offsetFor pagination
# Last 50 successful rule edits in the past 24 hours
curl -s "http://localhost:8080/api/v1/audit?event_type=rule.updated&result=success&from=$(date -u -d '24 hours ago' --iso-8601=seconds)" \
-H "Authorization: Bearer $TOKEN" | jq

# All login failures in the past week
curl -s "http://localhost:8080/api/v1/audit?event_type=auth.login&result=failure&from=$(date -u -d '7 days ago' --iso-8601=seconds)" \
-H "Authorization: Bearer $TOKEN" | jq

Web UI

The bundled Leptos admin has an Audit page with filter controls for each query field, infinite scroll for pagination, and a JSON detail expand on each row. Direct deep links preserve filter state via URL query params, so a security finding can be shared by URL.

hc-cli

hc-cli audit --event-type rule.deleted --since 24h

Output is JSON or a table depending on the global --output setting.

Per-role visibility

Audit access is scope-gated, not role-gated, so any role with audit:read can query. Out of the box that's:

Roleaudit:read
Adminyes (all scopes)
Observeryes
ServiceOperatoryes
RuleAuthorno
DeviceOperatorno
Guestno
Disabledno

Scope grants on custom roles can be edited via PATCH /users/{id} or the Admin page.

Retention

By default 365 days of history is kept. The pruner runs every 6 hours and deletes anything older than the cutoff. Tune via:

[auth]
audit_retention_days = 90 # 0 to disable pruning

For long-term archival, run POST /system/backup periodically and keep the resulting archives — they include audit.db alongside the state and history databases.

Implementation notes

  • Storage is SQLite via rusqlite. Indexes cover the common filter columns (ts, actor_id, event_type, target_kind+target_id) so even multi-million-row logs query in tens of milliseconds.
  • Recording is best-effort and does not block the originating operation. If you need stricter guarantees (e.g. financial-grade immutability) the file-level approach via syslog forwarding is the recommended path.
  • The correlation_id field links events from a single request chain — useful when one API call spawns multiple downstream mutations (e.g. a rule import that creates a rule, links it to a group, and enables it).