import {Request} from '@loopback/rest'; import {FlclLoggerLb4} from 'fc-logger'; import {DEFAULT_LOGGING_CONFIG} from '../constants/security-defaults'; import {ErrorResponseConfig, LoggingConfig, SecurityEvent} from '../types'; import {SecurityError} from './security-error'; /** * Security logger utility for centralized logging of security events */ export class SecurityLogger { constructor( private config?: LoggingConfig, private logger?: FlclLoggerLb4, private errorResponseConfig?: ErrorResponseConfig ) { // Set default config if not provided this.config = { ...DEFAULT_LOGGING_CONFIG, ...config }; } /** * Log a security event */ logSecurityEvent( req: Request | Record, event: SecurityEvent, className?: string, methodName?: string ): void { if (!this.config?.enabled) { return; } const enrichedEvent = this.enrichEvent(req, event); const maskedEvent = this.config.maskSensitiveData ? this.maskSensitiveData(enrichedEvent) : enrichedEvent; // Determine if we should log this event based on configuration if (!this.shouldLogEvent(maskedEvent)) { return; } // Use the injected logger if available, otherwise console if (this.logger) { this.logWithFlclLogger(maskedEvent, className, methodName); } else { this.logWithConsole(maskedEvent); } } /** * Log security violation with high priority */ logSecurityViolation( req: Request | Record, error: Error | string, metadata?: Record, className?: string, methodName?: string ): void { const violationEvent: SecurityEvent = { timestamp: new Date().toISOString(), eventType: 'SECURITY_VIOLATION', severity: 'HIGH', error: error instanceof Error ? error.message : error, metadata: { ...metadata, stack: error instanceof Error ? error.stack : undefined } }; this.logSecurityEvent(req, violationEvent, className, methodName); } /** * Log malicious activity detection */ logMaliciousActivity( req: Request | Record, activityType: string, details: string, metadata?: Record, className?: string, methodName?: string ): void { const maliciousEvent: SecurityEvent = { timestamp: new Date().toISOString(), eventType: 'MALICIOUS_ACTIVITY', severity: 'CRITICAL', error: `${activityType}: ${details}`, metadata }; this.logSecurityEvent(req, maliciousEvent, className, methodName); } /** * Enrich event with request details */ private enrichEvent(req: Request | Record, event: SecurityEvent): SecurityEvent { const enriched = {...event}; // Extract request details if available if (this.isRequest(req)) { enriched.ip = req.ip ?? ((req as unknown as Record).connection as Record)?.remoteAddress as string; enriched.userAgent = req.headers?.['user-agent']; enriched.url = req.originalUrl ?? req.url; enriched.method = req.method; enriched.requestId = req.headers?.['x-request-id'] as string; enriched.rootRequestId = req.headers?.['root-request-id'] as string || req.headers?.['x-root-request-id'] as string; } else { // Handle generic object enriched.ip = req.ip as string; enriched.userAgent = req.userAgent as string; enriched.url = req.url as string; enriched.method = req.method as string; enriched.requestId = req.requestId as string; enriched.rootRequestId = req.rootRequestId as string; } return enriched; } /** * Check if object is a Request */ private isRequest(obj: Request | Record): obj is Request { return obj && typeof obj === 'object' && 'method' in obj && 'url' in obj; } /** * Mask sensitive data in the event */ private maskSensitiveData(event: SecurityEvent): SecurityEvent { if (!this.config?.sensitiveFields) { return event; } const maskedEvent = {...event}; const sensitiveFields = this.config.sensitiveFields; // Recursively mask sensitive fields const maskObject = (obj: Record): Record => { const masked = {...obj}; for (const key in masked) { const lowerKey = key.toLowerCase(); // Check if field name contains sensitive keywords if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) { masked[key] = '***MASKED***'; } else if (typeof masked[key] === 'object' && masked[key] !== null) { masked[key] = maskObject(masked[key] as Record); } } return masked; }; if (maskedEvent.metadata) { maskedEvent.metadata = maskObject(maskedEvent.metadata); } return maskedEvent; } /** * Determine if event should be logged based on configuration */ private shouldLogEvent(event: SecurityEvent): boolean { if (!this.config) { return true; } // Always log security violations and malicious activity if (event.eventType === 'SECURITY_VIOLATION' || event.eventType === 'MALICIOUS_ACTIVITY') { return true; } // Check specific event type configurations if (event.eventType === 'SECURITY_CHECK_PASSED' && !this.config.logSuccessfulRequests) { return false; } // Check log level const eventLogLevel = this.getEventLogLevel(event); const configLogLevel = this.getLogLevelValue(this.config.logLevel ?? 'info'); return this.getLogLevelValue(eventLogLevel) >= configLogLevel; } /** * Get appropriate log level for event */ private getEventLogLevel(event: SecurityEvent): 'error' | 'warn' | 'info' | 'debug' { switch (event.severity) { case 'CRITICAL': return 'error'; case 'HIGH': return 'error'; case 'MEDIUM': return 'warn'; case 'LOW': default: return 'info'; } } /** * Convert log level to numeric value for comparison */ private getLogLevelValue(level: 'error' | 'warn' | 'info' | 'debug'): number { const levels = { error: 3, warn: 2, info: 1, debug: 0 }; return levels[level] || 1; } /** * Log using FlclLogger */ private logWithFlclLogger(event: SecurityEvent, className?: string, methodName?: string): void { const logLevel = this.getEventLogLevel(event); const message = `Security Event: ${event.eventType}${event.error ? ` - ${event.error}` : ''}`; // Prepare log data for FlclLogger const logData = { message, data: { ...event, // Include root-request-id if available rootRequestId: event.rootRequestId, requestId: event.requestId, timestamp: event.timestamp, eventType: event.eventType, severity: event.severity, ip: event.ip, userAgent: event.userAgent, url: event.url, method: event.method, metadata: event.metadata } }; // Use className and methodName if provided if (className && methodName) { switch (logLevel) { case 'error': this.logger?.error({ className, methodName, ...logData }); break; case 'warn': this.logger?.warn({ className, methodName, ...logData }); break; case 'info': this.logger?.info({ className, methodName, ...logData }); break; case 'debug': default: this.logger?.debug({ className, methodName, ...logData }); break; } } else { // Fallback to simple message logging when className/methodName not provided switch (logLevel) { case 'error': this.logger?.error(logData); break; case 'warn': this.logger?.warn(logData); break; case 'info': this.logger?.info(logData); break; case 'debug': default: this.logger?.debug(logData); break; } } } /** * Log using console (fallback) */ private logWithConsole(event: SecurityEvent): void { const logLevel = this.getEventLogLevel(event); const timestamp = new Date().toISOString(); const message = `[${timestamp}] FC-Security ${logLevel.toUpperCase()}: ${event.eventType}`; const logData = { ...event, message }; switch (logLevel) { case 'error': console.error(message, logData); break; case 'warn': console.warn(message, logData); break; case 'info': console.info(message, logData); break; case 'debug': default: console.log(message, logData); break; } } /** * Handle SecurityError - logs detailed information internally and returns safe public response */ handleSecurityError( req: Request, error: SecurityError ): { shouldLog: boolean; publicResponse: Record; loggedDetails: Record; } { // Always log security errors with full details const loggedDetails = { ...error.getDetails(), request: { ip: req.ip, method: req.method, url: req.url, userAgent: req.get('user-agent'), timestamp: new Date().toISOString() } }; // Log the detailed error information this.logSecurityViolation(req, error.message, { errorType: error.type, severity: error.severity, statusCode: error.statusCode, metadata: error.metadata }); // Return public response based on configuration const publicResponse = error.getPublicResponseWithConfig(this.errorResponseConfig); return { shouldLog: true, publicResponse, loggedDetails }; } /** * Generic method to handle any error safely */ handleGenericError( req: Request, error: unknown, fallbackMessage = 'Something went wrong' ): { shouldLog: boolean; publicResponse: Record; loggedDetails: Record; } { if (error instanceof SecurityError) { return this.handleSecurityError(req, error); } // Handle other types of errors const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; const loggedDetails = { error: errorMessage, stack: errorStack, request: { ip: req.ip, method: req.method, url: req.url, userAgent: req.get('user-agent'), timestamp: new Date().toISOString() } }; // Log the error if (this.logger) { this.logger.error({ className: 'SecurityLogger', methodName: 'handleGenericError', message: `Unhandled error: ${errorMessage}`, data: loggedDetails }); } else { console.error('Unhandled error:', loggedDetails); } // Return generic public response with configuration support const shouldExposeDetailedErrors = this.errorResponseConfig?.exposeDetailedErrors ?? false; const customErrorMessage = this.errorResponseConfig?.customErrorMessage ?? fallbackMessage; const publicResponse = { error: 'INTERNAL_ERROR', message: shouldExposeDetailedErrors ? errorMessage : customErrorMessage, statusCode: 400, timestamp: new Date().toISOString(), // Include stack trace only if detailed errors are enabled ...(shouldExposeDetailedErrors && errorStack && { stack: errorStack }) }; return { shouldLog: true, publicResponse, loggedDetails }; } /** * Update logger configuration */ updateConfig(config: Partial): void { this.config = {...this.config, ...config}; } /** * Get current configuration */ getConfig(): LoggingConfig | undefined { return this.config; } }