Skip to main content

Audit Logging

ESUS automatically logs every authenticated request to an append-only audit log. Audit events are stored with full context: who performed the action, what resource was accessed, when, and with what outcome.

What is Logged Automatically

  • Authentication events (login, logout, failed attempts)
  • All FHIR resource reads, writes, updates, and deletes
  • Emergency override accesses
  • Consent changes
  • Bulk export operations
  • Role and permission changes

FHIR resource-access logging is written synchronously within the request itself (via a database transaction awaited inline before the response is sent) — audit event persistence is guaranteed as part of the request flow.

Query Audit Logs

# Search all audit logs (with optional filters)
curl "https://api.esus.health/admin/audit" \
  -H "Authorization: Bearer TOKEN"

# Get audit logs for a specific patient
curl "https://api.esus.health/admin/audit/patient/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -H "Authorization: Bearer TOKEN"

# Get audit logs for a specific user
curl "https://api.esus.health/admin/audit/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "Authorization: Bearer TOKEN"

Audit Event Structure

Each audit event contains:

{
  "id": "e5f6a7b8-c9d0-1234-ef56-78901234abcd",
  "organizationId": "9a8b7c6d-5e4f-3210-9876-543210fedcba",
  "type": "rest",
  "subtype": "read",
  "action": "R",
  "outcome": "success",
  "recorded": "2024-01-15T10:30:00Z",
  "agent": {
    "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "email": "dr.smith@hospital.com",
    "ip": "203.0.113.42",
    "userAgent": "curl/8.4.0"
  },
  "entity": {
    "resourceType": "Patient",
    "resourceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "query": null
  }
}

The type field is always "rest" for resource access; the subtype indicates the operation (read, create, update, delete, execute). The agent object holds userId, email, ip, and userAgent (it does not include roles or an emergency-override flag). The entity object holds resourceType, resourceId, and query (for searches).

Action Codes

CodeMeaning
CCreate
RRead
UUpdate
DDelete
EExecute (e.g., $everything, $export)

Outcome Values

ValueMeaning
successAction completed successfully
failureAction was denied or failed (403, 404, server error, etc.)

Organization Activity Summary

# Recent audit activity for your organization
curl "https://api.esus.health/admin/activity" \
  -H "Authorization: Bearer TOKEN"

# Organization-wide stats
curl "https://api.esus.health/admin/stats" \
  -H "Authorization: Bearer TOKEN"

Subscribing to Audit Events

You can receive real-time webhooks for security-relevant events using the FHIR Subscription resource:

# Create a webhook subscription for failed access attempts
curl -X POST https://api.esus.health/fhir/Subscription \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -d '{
    "resourceType": "Subscription",
    "status": "active",
    "criteria": "AuditEvent?outcome=failure",
    "channel": {
      "type": "rest-hook",
      "endpoint": "https://your-app.com/security-alerts",
      "payload": "application/fhir+json"
    }
  }'

Bulk Export of Audit Data

To export audit logs for compliance reporting, use the FHIR Bulk Export system:

# Kick off a system-level bulk export (includes AuditEvent resources)
curl "https://api.esus.health/fhir/$export?_type=AuditEvent" \
  -H "Authorization: Bearer TOKEN" \
  -H "Accept: application/fhir+json" \
  -H "Prefer: respond-async"

See Bulk Export for details on polling for results.

Compliance Notes

  • Audit records are append-only — they cannot be modified or deleted, even by admins
  • Each row is chained with a keyed HMAC hash (tamper-evident hash chain), so any alteration or deletion of a row breaks the chain and is detectable
  • The audit table has no Row-Level Security — all admins can query the full audit trail
  • Audit logs are retained according to your organization’s retention policy
  • Emergency override accesses are recorded with a tagged resourceType of Resource[emergency] in the entity field (there is no emergencyOverride flag)