Introduction
Audit logs record security-relevant events for compliance, forensics, and monitoring. They answer "who did what, when, and from where?" Essential for detecting breaches, investigating incidents, and meeting regulatory requirements.
Key Concepts
- Audit Trail: Chronological record of system activities
- Non-repudiation: Users can't deny their actions
- Immutability: Logs can't be modified after creation
- Correlation ID: Links related events across services
Real World Context
Audit logging required for:
- GDPR, HIPAA, SOC2 compliance
- Breach investigation and forensics
- User activity monitoring
- Security incident detection
Deep Dive
Audit Log Entity
Design the audit log entity to capture who, what, when, where, and the outcome. The jsonb metadata column allows storing context-specific details without schema changes.
typescript@Entity() export class AuditLog { @PrimaryGeneratedColumn('uuid') id: string; @Column() action: string; // 'USER_LOGIN', 'PASSWORD_CHANGE', etc. @Column({ nullable: true }) userId: string; @Column({ nullable: true }) targetId: string; // ID of affected resource @Column() targetType: string; // 'User', 'Order', etc. @Column('jsonb', { nullable: true }) metadata: Record<string, any>; @Column() ipAddress: string; @Column({ nullable: true }) userAgent: string; @Column() correlationId: string; @Column({ type: 'enum', enum: ['SUCCESS', 'FAILURE'] }) outcome: 'SUCCESS' | 'FAILURE'; @CreateDateColumn() createdAt: Date; }
The correlationId links related events across services, making it possible to trace a complete user journey through a distributed system.
Audit Service
The service provides both a generic log() method and a convenience logSecurityEvent() method that automatically extracts context from the request.
typescript@Injectable() export class AuditService { constructor( @InjectRepository(AuditLog) private auditRepo: Repository<AuditLog>, ) {} async log(entry: CreateAuditLogDto) { await this.auditRepo.save({ ...entry, createdAt: new Date(), }); } async logSecurityEvent( action: string, request: Request, outcome: 'SUCCESS' | 'FAILURE', metadata?: Record<string, any>, ) { await this.log({ action, userId: request.user?.id, ipAddress: request.ip, userAgent: request.headers['user-agent'], correlationId: request.headers['x-correlation-id'] as string, outcome, metadata, }); } }
The request.user?.id uses optional chaining because some security events (like failed logins) may not have an authenticated user.
Audit Decorator and Interceptor
Automate audit logging with a custom @Audit() decorator and an interceptor that logs both successful and failed operations with timing data.
typescriptexport const Audit = (action: string) => SetMetadata('audit:action', action); @Injectable() export class AuditInterceptor implements NestInterceptor { constructor( private reflector: Reflector, private auditService: AuditService, ) {} intercept(context: ExecutionContext, next: CallHandler) { const action = this.reflector.get<string>('audit:action', context.getHandler()); if (!action) return next.handle(); const request = context.switchToHttp().getRequest(); const startTime = Date.now(); return next.handle().pipe( tap(async () => { await this.auditService.logSecurityEvent(action, request, 'SUCCESS', { duration: Date.now() - startTime, }); }), catchError(async (error) => { await this.auditService.logSecurityEvent(action, request, 'FAILURE', { error: error.message, duration: Date.now() - startTime, }); throw error; }), ); } } // Usage @Audit('USER_DELETE') @Delete(':id') deleteUser(@Param('id') id: string) { ... }
The interceptor captures both tap (success) and catchError (failure) outcomes, and always re-throws the error so normal error handling is not disrupted.
Critical Events to Log
Here is a comprehensive list of security events organized by category that should be logged in any production application.
typescript// Authentication events @Audit('USER_LOGIN') @Audit('USER_LOGOUT') @Audit('LOGIN_FAILED') @Audit('PASSWORD_CHANGE') @Audit('PASSWORD_RESET_REQUEST') @Audit('MFA_ENABLED') @Audit('MFA_DISABLED') // Authorization events @Audit('PERMISSION_DENIED') @Audit('ROLE_ASSIGNED') @Audit('ROLE_REMOVED') // Data events @Audit('USER_CREATED') @Audit('USER_DELETED') @Audit('SENSITIVE_DATA_ACCESSED') @Audit('DATA_EXPORTED') // Admin events @Audit('SETTINGS_CHANGED') @Audit('API_KEY_CREATED') @Audit('API_KEY_REVOKED')
At minimum, log all failed authentication attempts (LOGIN_FAILED) and permission denials (PERMISSION_DENIED) — these are the strongest signals for detecting attacks.
Log Retention and Protection
Implement automated retention policies to comply with regulations. Archive logs before deletion to maintain an immutable historical record.
typescript@Injectable() export class AuditRetentionService { @Cron('0 0 * * *') // Daily async enforceRetention() { const retentionDays = this.configService.get('AUDIT_RETENTION_DAYS', 365); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); // Archive old logs before deletion await this.archiveOldLogs(cutoffDate); // Delete archived logs await this.auditRepo.delete({ createdAt: LessThan(cutoffDate), archived: true, }); } }
The two-step archive-then-delete approach ensures logs are never lost — they are moved to cold storage (e.g., S3) before being removed from the database.
Common Pitfalls
- Logging sensitive data: Don't log passwords, tokens, or PII.
- Mutable logs: Logs should be append-only.
- No correlation: Without correlation IDs, related events can't be linked.
Best Practices
- Log all authentication and authorization events
- Include correlation IDs for request tracing
- Never log sensitive data (passwords, tokens, PII)
- Store logs separately from application data
- Implement retention policies for compliance
- Monitor logs for suspicious patterns
Summary
- Log all security-relevant events: authentication, authorization, and sensitive data access
- Include correlation IDs, user context, IP addresses, and user-agent for traceability
- Use a custom
@Audit()decorator with an interceptor to automate audit logging on routes - Store logs immutably with appropriate retention policies for compliance (GDPR, HIPAA, SOC2)
Code Examples
@Entity()
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
action: string; // 'USER_LOGIN', 'PASSWORD_CHANGE', etc.
@Column({ nullable: true })
userId: string;
@Column({ nullable: true })
targetId: string; // ID of affected resource
@Column()
targetType: string; // 'User', 'Order', etc.
@Column('jsonb', { nullable: true })
metadata: Record<string, any>;
@Column()
ipAddress: string;
@Column({ nullable: true })
userAgent: string;
@Column()
correlationId: string;
@Column({ type: 'enum', enum: ['SUCCESS', 'FAILURE'] })
outcome: 'SUCCESS' | 'FAILURE';
@CreateDateColumn()
createdAt: Date;
}