Application Audit Logging¶
Statement¶
This page covers application-layer audit logging — the in-app
record of user actions, claim modifications, and admin operations.
It is distinct from cloud-control-plane audit logs (gcloud / IAM /
firewall changes), which are covered in
Audit logging (cloud) and have
400-day immutable retention.
The application's audit posture is partial:
- Per-claim review notes persist who said what about a claim, when, and tie a free-text note to an author.
- A per-claim evidence-processing timeline ("audit trail" endpoint) reconstructs upload → queue → analysis → verdict times per piece of evidence.
- A
webhook_audit_logtable captures every change to an org's webhook configuration with actor, previous config, new config, and timestamp. It has an org-scoped read endpoint and is the working prototype of the planned generalised audit-log pattern — but it covers exactly one surface area today. - There is no general-purpose audit-log table for the rest of the security-relevant user events: login success/failure, logout, password changes, role changes, user creation/deactivation, API-key creation/deletion, non-webhook org-config changes. These are visible at the application logging level (stdout / pm2) but are not captured as structured, queryable, retention-policied events.
Closing that gap — generalising the existing webhook_audit_log
pattern into an audit_log table with append-only inserts at every
security-relevant code path — is the largest follow-up on this page.
Implementation¶
What exists today¶
review_notes — claim review history¶
Schema (scripts/db/schema.sql line 168):
- Per-claim free-text notes with author (
author_name), text body (note), andcreated_at. - Indexed on
(claim_ref)and(created_at DESC)for the per-claim history view. - Written by
server/src/routes/notes.jsandserver/src/routes/actions.js.
This is sufficient for the analyst-facing "what did the team say about this claim?" question. It is not a security-event audit log — it does not record logins, role grants, or other out-of-claim-scope events.
GET /api/analysis/claim/:claimId/audit-trail¶
Defined in server/src/routes/analysis.js lines 112-150. Despite the
endpoint name, this is not a security audit trail. It is a
per-evidence processing timeline that joins evidence_items,
layover_results, and analysis_runs to surface upload time, queue
time, completion time, processing duration, and verdict. It is the
"how did this evidence move through the pipeline" view.
This page is honest about that name overlap so an auditor reading the codebase doesn't conclude we already have a structured audit log when we don't.
webhook_audit_log — webhook configuration history¶
Schema (scripts/db/migrations/008_threshold_rules_audit.sql plus
the org_ref column added in
scripts/db/migrations/015_webhook_config_org_scope.sql):
- Columns:
id,user_id(FK touserswithON DELETE SET NULLso the audit row survives a user delete),user_email,action(text, default'config_update'),previous_config(jsonb),new_config(jsonb),org_ref(FK toorganizations),changed_at. - Written from
server/src/routes/webhooks.jsafter every successful PUT to/api/webhooks/config— both the previous and the new full config snapshot are stored, so a row stands on its own as a complete diff. - Read via
GET /api/webhooks/audit-login the same file. Org-scoped by default; super-admins see all orgs or can pass?org_ref=to narrow. - Retention: rows persist indefinitely until manually deleted. No cleanup job, no retention policy.
- Not append-only enforced at the DB layer. Insert sites are the
only writers in the current code, but Postgres would accept an
UPDATEorDELETEfrom any code path that took the row's primary key. The proposed trigger in the planned-implementation section below would close that.
This is the existence proof that the generalised pattern works. The roadmap below is "do the same for the rest of the security-relevant surface area," not "design the table from scratch."
Application logs (stdout / pm2)¶
The Node application emits unstructured logs via console.log /
console.error to pm2's log files. These contain operational signals
(errors, request paths, occasional warnings). They are:
- Captured by pm2 on the VM.
- Not aggregated to Cloud Logging today (the VM has the
logging.logWriterrole on its service account, but the application does not use the Cloud Logging client library). - Not retention-policied at the application layer beyond pm2's local rotation defaults.
This is enough for present-day debugging and is not presented as an audit control.
What does not exist today¶
The following events have no structured, queryable, retention-policied audit record at the application layer. They are visible at most in pm2 logs for a short window:
- Login success / failure / lockout.
- Logout.
- Password set / change.
- User create / update / disable / re-enable / role change.
- API-key issuance / rotation / deletion.
- Org-level config changes other than webhook config —
organization profile edits, threshold-rule edits outside the
webhook surface, integration changes that don't go through
/api/webhooks/config. - Admin reads of sensitive endpoints (e.g.,
GET /api/users/:idwhich returnspassword_plaintoday — see Authentication for that gap). - Cross-org reads by super-admins.
The cloud audit log records the infrastructure equivalents (IAM mutations, secret access, VM lifecycle) but does not see application-level user actions.
What this gap means in practice¶
For SOC 2 Type I, a written record of "we know what's missing and we have a plan" is more valuable than a hastily-built half-implementation. This page is that record. Until the gap is closed, "who did what" for application-level actions can be reconstructed only partially from:
review_notes(claim-scoped, free-text).- The cloud audit log (infrastructure-scoped, not user-action-scoped).
- pm2 logs (best-effort, short retention, unstructured).
That is not a credible audit story for SOC 2 Type II or for any customer that asks "show me every action user X took last quarter."
Planned implementation¶
The minimal next step (named here so future-us has a target):
audit_logtable in Postgres — a generalisation of the existingwebhook_audit_log. Columns:id(uuid),event_type(text),actor_user_id(uuid,ON DELETE SET NULLso the row survives the user being deleted),actor_role(text),actor_org_ref(uuid),target_type(text),target_id(uuid),event_data(jsonb),ip_address(inet),user_agent(text),created_at(timestamptz).- Append-only insert at each security-relevant code path:
routes/auth.js(login OK / fail / logout),routes/users.js(create / update / role change / disable / password change),routes/apiKeys.js(issue / delete / scope change),routes/organizations.js(settings change). The webhook insert site inroutes/webhooks.jseither migrates to the unified table or double-writes for a transition period — the call is taken at implementation time. - Read endpoint
GET /api/audit-logwith org-scoping that mirrors the existingGET /api/webhooks/audit-logpattern — non-super-admins are pinned to their own org, super-admins see all orgs or pass?org_ref=to narrow. - Retention policy documented on this page once the table
exists. Initial proposal: 400 days, matching the cloud audit log,
to give a single retention number across both layers. The
existing
webhook_audit_logadopts the same retention. - Append-only enforcement at the DB layer — a Postgres trigger
that rejects
UPDATEandDELETEonaudit_log(and, retroactively, onwebhook_audit_log). Mutation becomes a code change, not a runtime operation.
Status¶
partial — verified 2026-04-30.
What's in place:
- Per-claim review notes with author and timestamp.
- Per-claim evidence-processing timeline ("audit-trail" endpoint).
webhook_audit_logtable with org-scoped read endpoint — one-surface working prototype of the planned generalised pattern.- pm2-captured application logs (best-effort, short retention).
- Cloud-side audit logs for infrastructure actions (covered in Audit logging (cloud)).
Known gaps¶
- No general-purpose
audit_logtable for the rest of the security-relevant surface area (auth events, user/role mutations, API-key lifecycle, non-webhook config changes). Largest gap. - No DB-level append-only enforcement anywhere in the
application audit story — including on
webhook_audit_log. Insert sites are the only writers in the current code, but Postgres would accept anUPDATEorDELETE. - No application-level retention policy for the events that are recorded (review notes, evidence timeline rows, webhook audit rows). They live until manually deleted.
- No central log aggregation. pm2 logs stay on the VM; they are not shipped to Cloud Logging today.
- No alerting on suspicious application events (failed-login spikes, privilege-escalation attempts, mass-export patterns).
- The "audit-trail" endpoint name is misleading — it's an evidence-processing timeline, not a security audit. Renaming it is queued.
Roadmap¶
audit_logtable plus append-only Postgres trigger as described above. This is the highest-priority application-security follow-up named on this page.- Inserts at every security-relevant code path with a documented event-type vocabulary.
- Org-scoped read endpoint for the audit log.
- Cloud Logging integration for application logs so pm2's local rotation isn't the retention story.
- Alerting on a defined event-type set (failed-login spikes, cross-org reads by super-admins, API-key creation outside a documented window).
- Rename the per-claim
audit-trailendpoint toevidence-timelineto free the term "audit" for the actual audit log when it ships.