From 6c28330af32b4e34005d48e37707dcc484a1686d Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Wed, 24 Sep 2025 04:57:07 +0200 Subject: [PATCH] Fix jwt-token --- data-retention-service/database.js | 37 +- data-retention-service/index.js | 28 +- .../20250924-create-security-logs.js | 124 ++++++ server/models/SecurityLog.js | 160 +++++++ server/routes/user.js | 24 + server/services/securityLogService.js | 420 ++++++++++++++++++ 6 files changed, 772 insertions(+), 21 deletions(-) create mode 100644 server/migrations/20250924-create-security-logs.js create mode 100644 server/models/SecurityLog.js create mode 100644 server/services/securityLogService.js diff --git a/data-retention-service/database.js b/data-retention-service/database.js index 5a01892..b60521d 100644 --- a/data-retention-service/database.js +++ b/data-retention-service/database.js @@ -154,42 +154,57 @@ function defineModels() { ] }); - // SecurityLog model (optional, might not exist in all installations) + // SecurityLog model - IMPORTANT: Security logs have different retention policies (much longer) models.SecurityLog = sequelize.define('SecurityLog', { id: { - type: DataTypes.INTEGER, + type: DataTypes.UUID, primaryKey: true, - autoIncrement: true + defaultValue: DataTypes.UUIDV4 }, tenant_id: { type: DataTypes.UUID, allowNull: true }, - timestamp: { - type: DataTypes.DATE, + event_type: { + type: DataTypes.STRING(50), allowNull: false }, - level: { + severity: { type: DataTypes.STRING(20), allowNull: false }, + username: { + type: DataTypes.STRING(100), + allowNull: true + }, + ip_address: { + type: DataTypes.INET, + allowNull: true + }, + country_code: { + type: DataTypes.STRING(2), + allowNull: true + }, message: { type: DataTypes.TEXT, allowNull: false }, - metadata: { - type: DataTypes.JSONB, - defaultValue: {} + created_at: { + type: DataTypes.DATE, + allowNull: false } }, { tableName: 'security_logs', timestamps: false, indexes: [ { - fields: ['tenant_id', 'timestamp'] + fields: ['tenant_id', 'created_at'] }, { - fields: ['timestamp'] + fields: ['event_type', 'created_at'] + }, + { + fields: ['ip_address', 'created_at'] } ] }); diff --git a/data-retention-service/index.js b/data-retention-service/index.js index 02721aa..690c1a7 100644 --- a/data-retention-service/index.js +++ b/data-retention-service/index.js @@ -165,11 +165,12 @@ class DataRetentionService { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - effectiveRetentionDays); - console.log(`๐Ÿงน Cleaning tenant ${tenant.slug} - removing data older than ${effectiveRetentionDays} days (before ${cutoffDate.toISOString()})`); + console.log(`๐Ÿงน Cleaning tenant ${tenant.slug} - removing operational data older than ${effectiveRetentionDays} days (before ${cutoffDate.toISOString()})`); + console.log(`๐Ÿ“‹ Note: Security logs and audit trails are preserved and not subject to automatic cleanup`); - const { DroneDetection, Heartbeat, SecurityLog } = await getModels(); + const { DroneDetection, Heartbeat } = await getModels(); - // Clean up drone detections + // Clean up drone detections (operational data) const deletedDetections = await DroneDetection.destroy({ where: { tenant_id: tenant.id, @@ -179,7 +180,7 @@ class DataRetentionService { } }); - // Clean up heartbeats + // Clean up heartbeats (operational data) const deletedHeartbeats = await Heartbeat.destroy({ where: { tenant_id: tenant.id, @@ -189,23 +190,30 @@ class DataRetentionService { } }); - // Clean up security logs (if they have tenant_id) + // Clean up security logs - MUCH LONGER retention (7 years for compliance) + // Security logs should only be cleaned up after 7 years, not the standard retention period let deletedLogs = 0; try { + const securityLogCutoffDate = new Date(); + securityLogCutoffDate.setFullYear(securityLogCutoffDate.getFullYear() - 7); // 7 years retention + deletedLogs = await SecurityLog.destroy({ where: { tenant_id: tenant.id, - timestamp: { - [Op.lt]: cutoffDate + created_at: { + [Op.lt]: securityLogCutoffDate } } }); + + if (deletedLogs > 0) { + console.log(`๐Ÿ” Cleaned ${deletedLogs} security logs older than 7 years for tenant ${tenant.slug}`); + } } catch (error) { - // SecurityLog might not have tenant_id field - console.log(`โš ๏ธ Skipping security logs for tenant ${tenant.slug}: ${error.message}`); + console.log(`โš ๏ธ Error cleaning security logs for tenant ${tenant.slug}: ${error.message}`); } - console.log(`โœ… Tenant ${tenant.slug}: Deleted ${deletedDetections} detections, ${deletedHeartbeats} heartbeats, ${deletedLogs} logs`); + console.log(`โœ… Tenant ${tenant.slug}: Deleted ${deletedDetections} detections, ${deletedHeartbeats} heartbeats, ${deletedLogs} security logs (7yr retention)`); return { detections: deletedDetections, diff --git a/server/migrations/20250924-create-security-logs.js b/server/migrations/20250924-create-security-logs.js new file mode 100644 index 0000000..efbb91b --- /dev/null +++ b/server/migrations/20250924-create-security-logs.js @@ -0,0 +1,124 @@ +const { DataTypes } = require('sequelize'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('security_logs', { + id: { + type: DataTypes.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + allowNull: false + }, + tenant_id: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'tenants', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + event_type: { + type: DataTypes.STRING(50), + allowNull: false + }, + severity: { + type: DataTypes.STRING(20), + allowNull: false, + defaultValue: 'info' + }, + user_id: { + type: DataTypes.UUID, + allowNull: true + }, + username: { + type: DataTypes.STRING(100), + allowNull: true + }, + ip_address: { + type: DataTypes.INET, + allowNull: true + }, + client_ip: { + type: DataTypes.INET, + allowNull: true + }, + user_agent: { + type: DataTypes.TEXT, + allowNull: true + }, + rdns: { + type: DataTypes.STRING(255), + allowNull: true + }, + country_code: { + type: DataTypes.STRING(2), + allowNull: true + }, + country_name: { + type: DataTypes.STRING(100), + allowNull: true + }, + city: { + type: DataTypes.STRING(100), + allowNull: true + }, + is_high_risk_country: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + message: { + type: DataTypes.TEXT, + allowNull: false + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true, + defaultValue: {} + }, + session_id: { + type: DataTypes.STRING(255), + allowNull: true + }, + request_id: { + type: DataTypes.STRING(255), + allowNull: true + }, + endpoint: { + type: DataTypes.STRING(255), + allowNull: true + }, + method: { + type: DataTypes.STRING(10), + allowNull: true + }, + status_code: { + type: DataTypes.INTEGER, + allowNull: true + }, + alerted: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + created_at: { + type: DataTypes.DATE, + defaultValue: Sequelize.NOW, + allowNull: false + } + }); + + // Add indexes for performance + await queryInterface.addIndex('security_logs', ['tenant_id', 'created_at']); + await queryInterface.addIndex('security_logs', ['event_type', 'created_at']); + await queryInterface.addIndex('security_logs', ['ip_address', 'created_at']); + await queryInterface.addIndex('security_logs', ['username', 'created_at']); + await queryInterface.addIndex('security_logs', ['severity', 'created_at']); + await queryInterface.addIndex('security_logs', ['country_code', 'is_high_risk_country']); + await queryInterface.addIndex('security_logs', ['alerted', 'severity', 'created_at']); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('security_logs'); + } +}; \ No newline at end of file diff --git a/server/models/SecurityLog.js b/server/models/SecurityLog.js new file mode 100644 index 0000000..f9759e2 --- /dev/null +++ b/server/models/SecurityLog.js @@ -0,0 +1,160 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const SecurityLog = sequelize.define('SecurityLog', { + id: { + type: DataTypes.UUID, + defaultValue: sequelize.Sequelize.UUIDV4, + primaryKey: true + }, + tenant_id: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'tenants', + key: 'id' + }, + comment: 'Tenant ID for multi-tenant isolation (null for system-wide events)' + }, + event_type: { + type: DataTypes.STRING(50), + allowNull: false, + comment: 'Type of security event (login_failed, login_success, suspicious_pattern, etc.)' + }, + severity: { + type: DataTypes.STRING(20), + allowNull: false, + defaultValue: 'info', + validate: { + isIn: [['low', 'medium', 'high', 'critical']] + }, + comment: 'Severity level of the security event' + }, + user_id: { + type: DataTypes.UUID, + allowNull: true, + comment: 'User ID if applicable' + }, + username: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'Username involved in the event' + }, + ip_address: { + type: DataTypes.INET, + allowNull: true, + comment: 'Client IP address' + }, + client_ip: { + type: DataTypes.INET, + allowNull: true, + comment: 'Real client IP (if behind proxy/load balancer)' + }, + user_agent: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'User agent string' + }, + rdns: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Reverse DNS lookup of IP address' + }, + country_code: { + type: DataTypes.STRING(2), + allowNull: true, + comment: 'ISO country code from IP geolocation' + }, + country_name: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'Country name from IP geolocation' + }, + city: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'City from IP geolocation' + }, + is_high_risk_country: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: 'Whether the country is flagged as high-risk' + }, + message: { + type: DataTypes.TEXT, + allowNull: false, + comment: 'Detailed description of the security event' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true, + defaultValue: {}, + comment: 'Additional event-specific data' + }, + session_id: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Session ID if applicable' + }, + request_id: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'Request ID for correlation' + }, + endpoint: { + type: DataTypes.STRING(255), + allowNull: true, + comment: 'API endpoint or URL involved' + }, + method: { + type: DataTypes.STRING(10), + allowNull: true, + comment: 'HTTP method' + }, + status_code: { + type: DataTypes.INTEGER, + allowNull: true, + comment: 'HTTP response status code' + }, + alerted: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: 'Whether super admins have been alerted about this event' + }, + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.Sequelize.NOW, + comment: 'When the event occurred' + } + }, { + tableName: 'security_logs', + timestamps: true, + createdAt: 'created_at', + updatedAt: false, // Security logs should not be updated + indexes: [ + { + fields: ['tenant_id', 'created_at'] + }, + { + fields: ['event_type', 'created_at'] + }, + { + fields: ['ip_address', 'created_at'] + }, + { + fields: ['username', 'created_at'] + }, + { + fields: ['severity', 'created_at'] + }, + { + fields: ['country_code', 'is_high_risk_country'] + }, + { + fields: ['alerted', 'severity', 'created_at'] + } + ] + }); + + return SecurityLog; +}; \ No newline at end of file diff --git a/server/routes/user.js b/server/routes/user.js index 7eff99e..d39e5c3 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -391,9 +391,14 @@ router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => { * Called by multi-tenant auth middleware */ async function loginLocal(req, res, next) { + const securityLogger = require('../services/securityLogService'); + try { // Validate required fields if (!req.body.username || !req.body.password) { + // Log validation failure + await securityLogger.logFailedLogin(req, req.body.username || 'unknown', 'Missing credentials'); + return res.status(400).json({ success: false, message: 'Validation error for field' + @@ -444,6 +449,10 @@ async function loginLocal(req, res, next) { if (!user) { console.log(`โŒ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - User not found`); + + // Log failed login attempt + await securityLogger.logFailedLogin(req, username, 'User not found'); + return res.status(401).json({ success: false, message: 'Invalid credentials' @@ -452,6 +461,10 @@ async function loginLocal(req, res, next) { if (!user.is_active) { console.log(`โŒ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - Account is inactive`); + + // Log failed login attempt + await securityLogger.logFailedLogin(req, username, 'Account is inactive'); + return res.status(401).json({ success: false, message: 'Account is inactive' @@ -462,6 +475,10 @@ async function loginLocal(req, res, next) { if (!passwordMatch) { console.log(`โŒ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - Invalid password`); + + // Log failed login attempt + await securityLogger.logFailedLogin(req, username, 'Invalid password'); + return res.status(401).json({ success: false, message: 'Invalid credentials' @@ -470,6 +487,13 @@ async function loginLocal(req, res, next) { console.log(`โœ… Authentication successful for "${username}" in tenant "${req.tenant?.id}"`); + // Check if this IP had recent failed attempts + const clientIP = securityLogger.getRealClientIP(req) || securityLogger.getClientIP(req); + const hadRecentFailures = securityLogger.hadRecentFailedAttempts(clientIP); + + // Log successful login + await securityLogger.logSuccessfulLogin(req, user, hadRecentFailures); + // Update last login await user.update({ last_login: new Date() }); diff --git a/server/services/securityLogService.js b/server/services/securityLogService.js new file mode 100644 index 0000000..8ad1086 --- /dev/null +++ b/server/services/securityLogService.js @@ -0,0 +1,420 @@ +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(); \ No newline at end of file