Audit logging
Delphi records privileged and security-relevant actions as audit rows so operators can reconstruct who did what, from where, and when. Every row carries the per-event fields required by BSR 4.1.1 / 4.1.2: timestamp, event type, the origin IP address, the actor's user ID, and (where applicable) a human-readable name and a self-describing changedBy block.
This page describes the data model and the events the platform emits. For the Super Admin UI that reads these rows, see Access logs; for forwarding them to an external SIEM, see Security event export.
Where audit rows land
All audit rows are written to the same AuditLog Prisma table, regardless of which code path produced them. Two write paths feed it:
| Path | Entry point | Sink | Used for |
|---|---|---|---|
| DB audit | logAudit (@delphi/api → AuditLogFactory / AuditLoggingService / DatabaseLoggingStrategy) | AuditLog table only | CRUD on managed resources — users, teams, apps, endpoints, SIP/egress trunks, platform settings, base numbers, tools, flow definitions. |
| Security/auth event | logSecurityEvent / logAuth (@delphi/logger StructuredLogger → createPrismaLogPersistenceSink) | AuditLog table and canonical OTLP to SigNoz | Authentication (login/logout/password/lockout), rejected connections, access denials, privilege escalation, security-policy changes, security-log access. |
Because the security/auth path also ships the record over OTLP, those rows are the ones available for SIEM export and appear as recordKind: Audit in SigNoz. The DB-only logAudit path is queryable through the Access logs UI.
Per-event fields (BSR 4.1.1 / 4.1.2)
Each row has top-level columns (indexed, used for filtering) and a JSON details block (the self-describing payload).
Top-level columns
| Column | Source | Notes |
|---|---|---|
time | row creation timestamp | UTC. Satisfies the BSR time / date requirement. |
action | the event type (LOGIN_FAILED, PRIVILEGE_ESCALATION_ATTEMPT, …) | Satisfies the BSR type of event requirement. See the action catalog. |
userId | the actor's session user id, or 'anonymous' / 'unknown' | Satisfies the BSR user ID requirement. Anonymous for failed-auth paths. |
username | the actor's full name ("First Last"), falling back to email | Human-readable actor. Consistent across all security/CRUD call sites. |
category | AUTH, ROLE_MANAGEMENT, SECURITY, SYSTEM, … | Groups events in the UI and SIEM routing. |
entityId / appId | the affected resource, when applicable | Used for resource-scoped correlation. |
details block
The details JSON object carries the self-describing payload. The actor-context helper injects the security-relevant fields:
| Field | Meaning | Injected by |
|---|---|---|
ip | Origin IP of the request (x-forwarded-for first value, else x-real-ip). BSR IP address of origin. | getActorDetails(ctx) |
userAgent | Origin User-Agent header. | getActorDetails(ctx) |
email | Actor's email. | getActorDetails(ctx) |
userName | Actor's full name ("First Last", or email if names are missing, or "unknown"). | getActorDetails(ctx) |
changedBy | { userId, name, email } — self-describing who performed the change, for SOC escalation. | getActorDetails(ctx) |
old / new | Before/after snapshot for CRUD rows. | the call site |
reason | Free-text reason for denials / failures. | the call site |
MSISDN is recorded in the details block only for telephony-side events; the web/admin audit surface does not carry one (BSR MSISDN is N/A there).
Actor context and origin injection
Call sites do not assemble email / userName / changedBy / ip / userAgent by hand. They spread getActorDetails(ctx) into the details object and the helper fills them in:
import {getActor, getActorDetails} from '@delphi/api/services/logging/actorContext';
logSecurityEvent(
'PRIVILEGE_ESCALATION_ATTEMPT',
{id: ctx.session.user.id, username: getActor(ctx)?.userName ?? undefined},
{
targetUserId: input.userId,
oldRole: teamUser.role,
newRole: input.role,
...getActorDetails(ctx), // email, userName, changedBy, ip, userAgent
},
'info',
);
getActorDetails reads the origin even for unauthenticated contexts, so failed-login, rejected-connection, and access-denied rows still carry the requesting IP and User-Agent per BSR 4.1.2 — there is no actor in those paths, but there is always an origin.
The origin is derived from request headers by getRequestMeta:
ip— first value ofx-forwarded-for, elsex-real-ipuserAgent— theuser-agentheaderrequestId—x-request-idorx-correlation-id(carried for correlation, not a BSR field)
getActorDetails lives in @delphi/api and reads headers from the tRPC ctx. The @delphi/auth package carries its own equivalent (getRequestMeta) used by the authentication handlers; both derive the origin the same way.
What is audited (BSR scope)
The platform audits the security-event scope defined for Delphi. The table maps each scope item to the action emitted and where the origin / actor come from.
| Scope | Event | Action | Origin / actor |
|---|---|---|---|
| b | Failed login | LOGIN_FAILED | IP/UA from the auth handler; user id of the attempted account (or anonymous). |
| c | Privilege escalation / any role change | PRIVILEGE_ESCALATION_ATTEMPT, PLATFORM_ROLE_CHANGE_* | getActorDetails(ctx) — full name + changedBy + IP/UA for every role change, not only escalations to ADMIN. |
| d | Rejected connection (unauthenticated request to a protected op) | REJECTED_CONNECTION | tRPC securityAuditMiddleware — getActorDetails(ctx) injects IP/UA even for anonymous requests. |
| e | Access restriction violation (authenticated but forbidden) | ACCESS_DENIED | Same middleware — actor (if any) + IP/UA. |
| f | Ignore / done (access granted, no event) | — | Successful authorization produces no audit row by design. |
| h | Access to security logs | SECURITY_LOGS_ACCESSED | getActorDetails(ctx) — the Super Admin viewing the audit feed is itself audited with actor + IP/UA. |
| i | Security policy modification + SIP trunk modifications | SECURITY_POLICY_MODIFIED, CRUD UPDATE on SIP/egress trunks | getActorDetails(ctx) — actor full name + changedBy + IP/UA for both platform settings and trunk changes. |
Authentication successes and account-lockout events (VGWK-90 / BSR 2.4.7) are also recorded: LOGIN, LOGOUT, SESSION_END, PASSWORD_CHANGE, PASSWORD_RESET_REQUESTED / _SUCCEEDED, ACCOUNT_LOCKED / _UNLOCKED.
Action catalog
@delphi/logger defines the action constants (AuditActionTypes / SecurityEventAction):
| Action | Category | When |
|---|---|---|
LOGIN / LOGIN_FAILED | AUTH | Successful / failed sign-in. |
LOGOUT / SESSION_END | AUTH | Sign-out / session timeout or admin-forced end. |
PASSWORD_CHANGE | AUTH | Password changed (self or admin). |
PASSWORD_RESET_REQUESTED / _SUCCEEDED | AUTH | Reset flow stages. |
ACCOUNT_LOCKED / ACCOUNT_UNLOCKED | AUTH | Auto-lockout after threshold failures / unlock on reset. |
REJECTED_CONNECTION | SYSTEM | Unauthenticated request rejected by a protected procedure. |
ACCESS_DENIED | SECURITY | Authenticated request forbidden (insufficient role, expired password, Super Viewer write). |
PRIVILEGE_ESCALATION_ATTEMPT | ROLE_MANAGEMENT | A team role was changed (any direction); flagged on escalation to ADMIN. |
PLATFORM_ROLE_CHANGE_STARTED / _SUCCEEDED / _DENIED | ROLE_MANAGEMENT | Platform-wide role grant/change/revoke lifecycle. |
SECURITY_LOGS_ACCESSED | SECURITY | Super Admin opened the Access logs feed. |
SECURITY_POLICY_MODIFIED | SECURITY | A platform security setting was changed. |
SYSTEM_MANIPULATION_ATTEMPT | SECURITY | Tamper / manipulation attempt detected. |
SUPER_ADMIN_RECOVERY_ELEVATED / _DENIED | SECURITY | Break-glass elevation by the setup/recovery command. |
PASSWORD_RESET_BY_SETUP_CLI | AUTH | Audited Standard User recovery performed by the setup CLI. |
CREATE / UPDATE / DELETE | (per resource) | CRUD on managed resources via logAudit. |
Searching and displaying rows
The Access logs UI searches across both the top-level username column and the details block (details.email, details.actor.email, details.targetEmail, details.ip, details.msisdn). Changing a row's top-level username to a full name therefore does not break email-based search — the email remains searchable through details.email, which getActorDetails injects.
The display column prefers details.userName (the full name) and falls back to the top-level username, then details.email, so the UI shows a human-readable name regardless of which field the call site populated.
Retention and export
- Retention — Tasker prunes audit rows older than 180 days by default (
AUDIT_LOG_RETENTION_DAYS); see Ops service. - SIEM export — Security/auth rows are also shipped over OTLP and can be fanned out to an external SIEM; see Security event export.
See also
- Access logs — Super Admin UI for the AUTH and ROLE_MANAGEMENT feed.
- Security event export — forwarding audit events to an external SIEM.
- Service logs — per-service log inventory, including where audit records land.
- Monitoring in SigNoz — telemetry plumbing and the audit record JSON schema.