Skip to content

Security Event Logging

Dedicated audit trail for security-sensitive actions, separate from the business activity log (odp_activity).

Architecture

  • Every request gets a correlationId via AsyncLocalStorage — set at the first onRequest hook, before token extraction
  • Source: x-request-id header (from load balancer/gateway) or crypto.randomUUID() fallback
  • Response includes x-request-id header for client-side correlation

Event Types

22 event types across 6 categories:

CategoryEvents
AuthenticationLOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, PASSWORD_CHANGED, PASSWORD_RESET_REQUESTED, SESSION_REVOKED
AuthorizationROLE_ASSIGNED, ROLE_REMOVED, PERMISSION_CHANGED
TokensTOKEN_CREATED, TOKEN_REVOKED
MFAMFA_ENABLED, MFA_DISABLED
ImpersonationIMPERSONATION_STARTED, IMPERSONATION_STOPPED
InfrastructureCONFIG_CHANGED, SSRF_BLOCKED, RATE_LIMIT_TRIGGERED, AUDIT_CHAIN_BROKEN, DRIFT_DETECTED

Severity Levels

LevelUsage
infoNormal operations (login success, password change)
warningSuspicious activity (rate limit hit, drift detected)
criticalSecurity incident (SSRF blocked, audit chain broken)

SecurityEventService

typescript
import { SecurityEventService } from '../security/events/service.js';
import { SecurityEventType } from '../security/events/types.js';

const service = new SecurityEventService({ knex, accountability });

await service.emit(SecurityEventType.LOGIN_SUCCESS, {
  severity: 'info',
  details: { method: 'local' },
});

Behavior:

  • Always logs to pino (synchronous, never blocks the request)
  • DB insert is fire-and-forget — errors are caught and logged, never propagated to callers
  • Auto-attaches correlation_id from the current AsyncLocalStorage context
  • Respects SECURITY_EVENTS_ENABLED toggle — when false, emit() returns immediately

Database Schema

sql
CREATE TABLE odp_security_events (
  id          BIGSERIAL PRIMARY KEY,
  type        VARCHAR(50)  NOT NULL,
  timestamp   TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  correlation_id VARCHAR(64),
  user_id     UUID REFERENCES odp_users(id),
  ip          VARCHAR(50),
  user_agent  TEXT,
  target_user UUID,
  collection  VARCHAR(255),
  details     JSONB,
  severity    VARCHAR(10)  NOT NULL DEFAULT 'info'
);

Indexed on: type, timestamp, user_id, correlation_id.

Pino Output Format

Events are logged as structured JSON to stdout for log aggregation pipelines:

json
{
  "level": 30,
  "time": 1718400000000,
  "msg": "Security event",
  "event": "SSRF_BLOCKED",
  "severity": "critical",
  "correlation_id": "a1b2c3d4-...",
  "user_id": null,
  "ip": "192.168.1.100",
  "details": {
    "url": "http://169.254.169.254/",
    "reason": "DNS resolved to private IP: 169.254.169.254"
  }
}

SIEM Integration

Recommended pipeline:

pino stdout → FluentBit / Filebeat → Elasticsearch / Splunk / Loki

Source Files

FilePurpose
src/security/events/types.tsEvent type constants + interfaces
src/security/events/service.tsSecurityEventService class
src/security/events/logger.tsPino security logger
src/security/events/correlation.tsAsyncLocalStorage correlation context
src/security/shared/constants.tsTable name constant

ODP Internal API Documentation