Appearance
Security Event Logging
Dedicated audit trail for security-sensitive actions, separate from the business activity log (odp_activity).
Architecture
- Every request gets a
correlationIdviaAsyncLocalStorage— set at the firstonRequesthook, before token extraction - Source:
x-request-idheader (from load balancer/gateway) orcrypto.randomUUID()fallback - Response includes
x-request-idheader for client-side correlation
Event Types
22 event types across 6 categories:
| Category | Events |
|---|---|
| Authentication | LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, PASSWORD_CHANGED, PASSWORD_RESET_REQUESTED, SESSION_REVOKED |
| Authorization | ROLE_ASSIGNED, ROLE_REMOVED, PERMISSION_CHANGED |
| Tokens | TOKEN_CREATED, TOKEN_REVOKED |
| MFA | MFA_ENABLED, MFA_DISABLED |
| Impersonation | IMPERSONATION_STARTED, IMPERSONATION_STOPPED |
| Infrastructure | CONFIG_CHANGED, SSRF_BLOCKED, RATE_LIMIT_TRIGGERED, AUDIT_CHAIN_BROKEN, DRIFT_DETECTED |
Severity Levels
| Level | Usage |
|---|---|
info | Normal operations (login success, password change) |
warning | Suspicious activity (rate limit hit, drift detected) |
critical | Security 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_idfrom the currentAsyncLocalStoragecontext - Respects
SECURITY_EVENTS_ENABLEDtoggle — whenfalse,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 / LokiSource Files
| File | Purpose |
|---|---|
src/security/events/types.ts | Event type constants + interfaces |
src/security/events/service.ts | SecurityEventService class |
src/security/events/logger.ts | Pino security logger |
src/security/events/correlation.ts | AsyncLocalStorage correlation context |
src/security/shared/constants.ts | Table name constant |