const dns = require('dns'); const { promisify } = require('util'); const reverseLookup = promisify(dns.reverse); class SecurityLogService { constructor() { // High-risk countries (ISO 2-letter codes) this.HIGH_RISK_COUNTRIES = new Set([ 'RU', // Russia 'CN', // China 'KP', // North Korea 'IR', // Iran 'BY', // Belarus // Add more as needed ]); // Cache for IP lookups to avoid repeated API calls this.ipCache = new Map(); this.rdnsCache = new Map(); // Suspicious activity tracking this.recentFailedLogins = new Map(); // IP -> array of timestamps this.recentSuccessfulLogins = new Map(); // IP -> array of timestamps // Clean up old tracking data every hour setInterval(() => this.cleanupTrackingData(), 60 * 60 * 1000); } /** * Log a security event */ async logSecurityEvent(eventData) { try { const SecurityLog = require('../models/SecurityLog'); // Enhance event data with IP information const enhancedData = await this.enhanceEventData(eventData); // Create the security log entry const securityLog = await SecurityLog.create(enhancedData); // Check for suspicious patterns if this is a login event if (eventData.event_type.includes('login')) { await this.checkSuspiciousPatterns(enhancedData); } console.log(`🔐 Security event logged: ${eventData.event_type} from ${eventData.ip_address}`); return securityLog; } catch (error) { console.error('❌ Failed to log security event:', error.message); // Don't throw - security logging failures shouldn't break the main application } } /** * Log failed login attempt */ async logFailedLogin(req, username, reason = 'Invalid credentials') { const eventData = { event_type: 'login_failed', severity: 'medium', username: username, ip_address: this.getClientIP(req), client_ip: this.getRealClientIP(req), user_agent: req.get('User-Agent'), endpoint: req.originalUrl || req.url, method: req.method, status_code: 401, message: `Failed login attempt for user '${username}': ${reason}`, metadata: { reason: reason, referer: req.get('Referer'), origin: req.get('Origin'), timestamp: new Date().toISOString() }, session_id: req.sessionID, request_id: req.id || req.get('X-Request-ID') }; // Track failed login for pattern detection const clientIP = eventData.client_ip || eventData.ip_address; this.trackFailedLogin(clientIP, username); return await this.logSecurityEvent(eventData); } /** * Log successful login */ async logSuccessfulLogin(req, user, wasAfterFailedAttempt = false) { const severity = wasAfterFailedAttempt ? 'high' : 'low'; const eventData = { event_type: 'login_success', severity: severity, user_id: user.id, username: user.username || user.email, ip_address: this.getClientIP(req), client_ip: this.getRealClientIP(req), user_agent: req.get('User-Agent'), endpoint: req.originalUrl || req.url, method: req.method, status_code: 200, message: `Successful login for user '${user.username || user.email}'${wasAfterFailedAttempt ? ' (after failed attempts)' : ''}`, metadata: { user_id: user.id, after_failed_attempt: wasAfterFailedAttempt, referer: req.get('Referer'), origin: req.get('Origin'), timestamp: new Date().toISOString() }, session_id: req.sessionID, request_id: req.id || req.get('X-Request-ID') }; // Track successful login for pattern detection const clientIP = eventData.client_ip || eventData.ip_address; this.trackSuccessfulLogin(clientIP); return await this.logSecurityEvent(eventData); } /** * Enhance event data with IP geolocation and reverse DNS */ async enhanceEventData(eventData) { const clientIP = eventData.client_ip || eventData.ip_address; if (clientIP) { // Get geolocation data const geoData = await this.getIPGeolocation(clientIP); if (geoData) { eventData.country_code = geoData.country_code; eventData.country_name = geoData.country_name; eventData.city = geoData.city; eventData.is_high_risk_country = this.HIGH_RISK_COUNTRIES.has(geoData.country_code); } // Get reverse DNS eventData.rdns = await this.getRDNS(clientIP); // Enhance logging for high-risk countries if (eventData.is_high_risk_country) { eventData.severity = this.escalateSeverity(eventData.severity); eventData.message += ` [HIGH-RISK COUNTRY: ${eventData.country_name}]`; } } return eventData; } /** * Get client IP address, handling proxies */ getClientIP(req) { return req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || req.connection?.socket?.remoteAddress || '127.0.0.1'; } /** * Get real client IP from headers (X-Forwarded-For, X-Real-IP, etc.) */ getRealClientIP(req) { const xForwardedFor = req.get('X-Forwarded-For'); if (xForwardedFor) { return xForwardedFor.split(',')[0].trim(); } return req.get('X-Real-IP') || req.get('X-Client-IP') || req.get('CF-Connecting-IP') || // Cloudflare this.getClientIP(req); } /** * Get IP geolocation (stub - implement with actual service) */ async getIPGeolocation(ip) { // Skip localhost and private IPs if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.')) { return { country_code: 'XX', country_name: 'Local/Private', city: 'Local' }; } // Check cache first if (this.ipCache.has(ip)) { return this.ipCache.get(ip); } try { // TODO: Integrate with actual IP geolocation service (MaxMind, ipapi, etc.) // For now, return a placeholder const geoData = { country_code: 'US', country_name: 'United States', city: 'Unknown' }; // Cache for 24 hours this.ipCache.set(ip, geoData); setTimeout(() => this.ipCache.delete(ip), 24 * 60 * 60 * 1000); return geoData; } catch (error) { console.error(`Failed to get geolocation for IP ${ip}:`, error.message); return null; } } /** * Get reverse DNS for IP */ async getRDNS(ip) { // Skip localhost and private IPs if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.')) { return 'localhost'; } // Check cache first if (this.rdnsCache.has(ip)) { return this.rdnsCache.get(ip); } try { const hostnames = await reverseLookup(ip); const rdns = hostnames[0] || null; // Cache for 1 hour this.rdnsCache.set(ip, rdns); setTimeout(() => this.rdnsCache.delete(ip), 60 * 60 * 1000); return rdns; } catch (error) { // RDNS lookup failed - not uncommon return null; } } /** * Track failed login for pattern detection */ trackFailedLogin(ip, username) { if (!this.recentFailedLogins.has(ip)) { this.recentFailedLogins.set(ip, []); } this.recentFailedLogins.get(ip).push({ timestamp: Date.now(), username: username }); // Keep only last 24 hours const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000); this.recentFailedLogins.set(ip, this.recentFailedLogins.get(ip).filter(attempt => attempt.timestamp > oneDayAgo) ); } /** * Track successful login for pattern detection */ trackSuccessfulLogin(ip) { if (!this.recentSuccessfulLogins.has(ip)) { this.recentSuccessfulLogins.set(ip, []); } this.recentSuccessfulLogins.get(ip).push(Date.now()); // Keep only last 24 hours const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000); this.recentSuccessfulLogins.set(ip, this.recentSuccessfulLogins.get(ip).filter(timestamp => timestamp > oneDayAgo) ); } /** * Check for suspicious login patterns */ async checkSuspiciousPatterns(eventData) { const clientIP = eventData.client_ip || eventData.ip_address; // Pattern 1: Successful login after multiple failed attempts if (eventData.event_type === 'login_success') { const failedAttempts = this.recentFailedLogins.get(clientIP) || []; const recentFailures = failedAttempts.filter(attempt => attempt.timestamp > (Date.now() - 60 * 60 * 1000) // Last hour ); if (recentFailures.length >= 3) { await this.alertSuperAdmins('suspicious_login_pattern', { pattern: 'successful_after_failures', ip: clientIP, username: eventData.username, failed_attempts: recentFailures.length, country: eventData.country_name, message: `Successful login for ${eventData.username} from ${clientIP} after ${recentFailures.length} failed attempts` }); } } // Pattern 2: Multiple failed logins from different IPs to different usernames (distributed attack) await this.checkDistributedAttack(); } /** * Check for distributed brute force attacks */ async checkDistributedAttack() { const now = Date.now(); const fiveMinutesAgo = now - (5 * 60 * 1000); let totalFailedLogins = 0; let uniqueIPs = new Set(); let uniqueUsernames = new Set(); for (const [ip, attempts] of this.recentFailedLogins.entries()) { const recentAttempts = attempts.filter(attempt => attempt.timestamp > fiveMinutesAgo); if (recentAttempts.length > 0) { totalFailedLogins += recentAttempts.length; uniqueIPs.add(ip); recentAttempts.forEach(attempt => uniqueUsernames.add(attempt.username)); } } // Alert if we see many failed logins from many IPs to many usernames if (totalFailedLogins >= 20 && uniqueIPs.size >= 5 && uniqueUsernames.size >= 5) { await this.alertSuperAdmins('distributed_attack', { pattern: 'distributed_brute_force', total_attempts: totalFailedLogins, unique_ips: uniqueIPs.size, unique_usernames: uniqueUsernames.size, message: `Distributed attack detected: ${totalFailedLogins} failed logins from ${uniqueIPs.size} IPs targeting ${uniqueUsernames.size} usernames in the last 5 minutes` }); } } /** * Alert super admins about suspicious activity */ async alertSuperAdmins(alertType, details) { try { // Log the alert as a critical security event await this.logSecurityEvent({ event_type: 'security_alert', severity: 'critical', message: `SECURITY ALERT: ${details.message}`, metadata: { alert_type: alertType, ...details }, alerted: true }); // TODO: Send actual notifications (email, SMS, Slack, etc.) console.log(`🚨 SECURITY ALERT - ${alertType.toUpperCase()}:`, details.message); } catch (error) { console.error('❌ Failed to alert super admins:', error.message); } } /** * Escalate severity level for high-risk events */ escalateSeverity(currentSeverity) { const severityLevels = { low: 1, medium: 2, high: 3, critical: 4 }; const reverseLevels = { 1: 'low', 2: 'medium', 3: 'high', 4: 'critical' }; const currentLevel = severityLevels[currentSeverity] || 1; const newLevel = Math.min(currentLevel + 1, 4); return reverseLevels[newLevel]; } /** * Clean up old tracking data */ cleanupTrackingData() { const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000); for (const [ip, attempts] of this.recentFailedLogins.entries()) { const filtered = attempts.filter(attempt => attempt.timestamp > oneDayAgo); if (filtered.length === 0) { this.recentFailedLogins.delete(ip); } else { this.recentFailedLogins.set(ip, filtered); } } for (const [ip, timestamps] of this.recentSuccessfulLogins.entries()) { const filtered = timestamps.filter(timestamp => timestamp > oneDayAgo); if (filtered.length === 0) { this.recentSuccessfulLogins.delete(ip); } else { this.recentSuccessfulLogins.set(ip, filtered); } } } /** * Check if IP recently had failed login followed by successful login */ hadRecentFailedAttempts(ip) { const failedAttempts = this.recentFailedLogins.get(ip) || []; const recentFailures = failedAttempts.filter(attempt => attempt.timestamp > (Date.now() - 60 * 60 * 1000) // Last hour ); return recentFailures.length > 0; } } module.exports = new SecurityLogService();