const fs = require('fs'); const path = require('path'); class SecurityLogger { constructor() { // Default to logs directory, but allow override via environment this.logDir = process.env.SECURITY_LOG_DIR || path.join(__dirname, '..', 'logs'); this.logFile = path.join(this.logDir, 'security-audit.log'); // Ensure log directory exists this.ensureLogDirectory(); // Initialize models reference (will be set when needed) this.models = null; } // Set models reference for database logging setModels(models) { this.models = models; } ensureLogDirectory() { try { if (!fs.existsSync(this.logDir)) { fs.mkdirSync(this.logDir, { recursive: true }); } } catch (error) { console.error('Failed to create log directory:', error.message); // Fallback to console logging only this.logFile = null; } } async logSecurityEvent(level, message, metadata = {}) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level: level.toUpperCase(), message, ...metadata }; // Always log to console for immediate visibility console.log(`[SECURITY AUDIT] ${timestamp} - ${message}`); // Also log to file if available if (this.logFile) { try { const logLine = JSON.stringify(logEntry) + '\n'; fs.appendFileSync(this.logFile, logLine); } catch (error) { console.error('Failed to write to security log file:', error.message); } } // Store in database if models are available if (this.models && this.models.AuditLog) { try { await this.models.AuditLog.create({ timestamp: new Date(), level: level.toUpperCase(), action: metadata.action || 'unknown', message, user_id: metadata.userId || null, username: metadata.username || null, tenant_id: metadata.tenantId || null, tenant_slug: metadata.tenantSlug || null, ip_address: metadata.ip || null, user_agent: metadata.userAgent || null, path: metadata.path || null, metadata: metadata, success: this.determineSuccess(level, metadata) }); } catch (error) { console.error('Failed to store audit log in database:', error.message); } } } determineSuccess(level, metadata) { // Determine if the action was successful based on level and metadata if (metadata.hasOwnProperty('success')) { return metadata.success; } // Assume success for info level, failure for error/critical switch (level.toUpperCase()) { case 'INFO': return true; case 'WARNING': return null; // Neutral case 'ERROR': case 'CRITICAL': return false; default: return null; } } logIPRestriction(ip, tenant, userAgent, denied = true) { const action = denied ? 'denied access to' : 'granted access to'; this.logSecurityEvent('WARNING', `IP ${ip} ${action} tenant ${tenant}`, { type: 'IP_RESTRICTION', ip, tenant, userAgent: userAgent || 'unknown', denied }); } logAuthFailure(reason, metadata = {}) { this.logSecurityEvent('ERROR', `Authentication failure: ${reason}`, { type: 'AUTH_FAILURE', reason, ...metadata }); } logSuspiciousActivity(activity, metadata = {}) { this.logSecurityEvent('CRITICAL', `Suspicious activity detected: ${activity}`, { type: 'SUSPICIOUS_ACTIVITY', activity, ...metadata }); } // Get recent security events for monitoring getRecentEvents(count = 100) { if (!this.logFile || !fs.existsSync(this.logFile)) { return []; } try { const content = fs.readFileSync(this.logFile, 'utf8'); const lines = content.trim().split('\n').filter(line => line); return lines .slice(-count) .map(line => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); } catch (error) { console.error('Failed to read security log file:', error.message); return []; } } } // Singleton instance const securityLogger = new SecurityLogger(); module.exports = { SecurityLogger, securityLogger };