Skip to main content
Version: 0.9.17-patch1

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:

PathEntry pointSinkUsed for
DB auditlogAudit (@delphi/apiAuditLogFactory / AuditLoggingService / DatabaseLoggingStrategy)AuditLog table onlyCRUD on managed resources — users, teams, apps, endpoints, SIP/egress trunks, platform settings, base numbers, tools, flow definitions.
Security/auth eventlogSecurityEvent / logAuth (@delphi/logger StructuredLogger → createPrismaLogPersistenceSink)AuditLog table and canonical OTLP to SigNozAuthentication (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

ColumnSourceNotes
timerow creation timestampUTC. Satisfies the BSR time / date requirement.
actionthe event type (LOGIN_FAILED, PRIVILEGE_ESCALATION_ATTEMPT, …)Satisfies the BSR type of event requirement. See the action catalog.
userIdthe actor's session user id, or 'anonymous' / 'unknown'Satisfies the BSR user ID requirement. Anonymous for failed-auth paths.
usernamethe actor's full name ("First Last"), falling back to emailHuman-readable actor. Consistent across all security/CRUD call sites.
categoryAUTH, ROLE_MANAGEMENT, SECURITY, SYSTEM, …Groups events in the UI and SIEM routing.
entityId / appIdthe affected resource, when applicableUsed for resource-scoped correlation.

details block

The details JSON object carries the self-describing payload. The actor-context helper injects the security-relevant fields:

FieldMeaningInjected by
ipOrigin IP of the request (x-forwarded-for first value, else x-real-ip). BSR IP address of origin.getActorDetails(ctx)
userAgentOrigin User-Agent header.getActorDetails(ctx)
emailActor's email.getActorDetails(ctx)
userNameActor'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 / newBefore/after snapshot for CRUD rows.the call site
reasonFree-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 of x-forwarded-for, else x-real-ip
  • userAgent — the user-agent header
  • requestIdx-request-id or x-correlation-id (carried for correlation, not a BSR field)
note

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.

ScopeEventActionOrigin / actor
bFailed loginLOGIN_FAILEDIP/UA from the auth handler; user id of the attempted account (or anonymous).
cPrivilege escalation / any role changePRIVILEGE_ESCALATION_ATTEMPT, PLATFORM_ROLE_CHANGE_*getActorDetails(ctx) — full name + changedBy + IP/UA for every role change, not only escalations to ADMIN.
dRejected connection (unauthenticated request to a protected op)REJECTED_CONNECTIONtRPC securityAuditMiddlewaregetActorDetails(ctx) injects IP/UA even for anonymous requests.
eAccess restriction violation (authenticated but forbidden)ACCESS_DENIEDSame middleware — actor (if any) + IP/UA.
fIgnore / done (access granted, no event)Successful authorization produces no audit row by design.
hAccess to security logsSECURITY_LOGS_ACCESSEDgetActorDetails(ctx) — the Super Admin viewing the audit feed is itself audited with actor + IP/UA.
iSecurity policy modification + SIP trunk modificationsSECURITY_POLICY_MODIFIED, CRUD UPDATE on SIP/egress trunksgetActorDetails(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):

ActionCategoryWhen
LOGIN / LOGIN_FAILEDAUTHSuccessful / failed sign-in.
LOGOUT / SESSION_ENDAUTHSign-out / session timeout or admin-forced end.
PASSWORD_CHANGEAUTHPassword changed (self or admin).
PASSWORD_RESET_REQUESTED / _SUCCEEDEDAUTHReset flow stages.
ACCOUNT_LOCKED / ACCOUNT_UNLOCKEDAUTHAuto-lockout after threshold failures / unlock on reset.
REJECTED_CONNECTIONSYSTEMUnauthenticated request rejected by a protected procedure.
ACCESS_DENIEDSECURITYAuthenticated request forbidden (insufficient role, expired password, Super Viewer write).
PRIVILEGE_ESCALATION_ATTEMPTROLE_MANAGEMENTA team role was changed (any direction); flagged on escalation to ADMIN.
PLATFORM_ROLE_CHANGE_STARTED / _SUCCEEDED / _DENIEDROLE_MANAGEMENTPlatform-wide role grant/change/revoke lifecycle.
SECURITY_LOGS_ACCESSEDSECURITYSuper Admin opened the Access logs feed.
SECURITY_POLICY_MODIFIEDSECURITYA platform security setting was changed.
SYSTEM_MANIPULATION_ATTEMPTSECURITYTamper / manipulation attempt detected.
SUPER_ADMIN_RECOVERY_ELEVATED / _DENIEDSECURITYBreak-glass elevation by the setup/recovery command.
PASSWORD_RESET_BY_SETUP_CLIAUTHAudited 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