/** * Log Management & Retention System (CWE-53 Compliance) * Automated cleanup, archival, integrity verification, and monitoring */ const logger = require('../utils/logger'); const logAggregator = require('../utils/logAggregator'); const SecurityAuditLogger = require('../utils/securityAudit'); const fs = require('fs').promises; const path = require('path'); const zlib = require('zlib'); const { promisify } = require('util'); const gzip = promisify(zlib.gzip); class LogManagement { constructor() { this.archiveDir = path.join(__dirname, '../../data/log-archives'); this.initialized = false; } /** * Initialize log management system */ async initialize() { if (this.initialized) return; try { // Ensure archive directory exists await fs.mkdir(this.archiveDir, { recursive: true, mode: 0o700 }); // Schedule daily log cleanup (runs at 2 AM) this.scheduleDailyCleanup(); // Schedule hourly integrity verification this.scheduleIntegrityChecks(); // Schedule weekly archival this.scheduleWeeklyArchival(); logger.info('[LogManagement] Initialized - Automated cleanup, archival, and integrity checks active'); this.initialized = true; } catch (error) { logger.error('[LogManagement] Failed to initialize:', error); } } /** * Schedule daily log cleanup at 2 AM */ scheduleDailyCleanup() { const scheduleNextCleanup = () => { const now = new Date(); const next2AM = new Date(); next2AM.setHours(2, 0, 0, 0); // If it's past 2 AM today, schedule for tomorrow if (now > next2AM) { next2AM.setDate(next2AM.getDate() + 1); } const msUntil2AM = next2AM - now; setTimeout(async () => { await this.performDailyCleanup(); scheduleNextCleanup(); // Schedule next day }, msUntil2AM); logger.info(`[LogManagement] Daily cleanup scheduled for ${next2AM.toISOString()}`); }; scheduleNextCleanup(); } /** * Schedule hourly integrity verification */ scheduleIntegrityChecks() { // Run immediately on startup this.verifyLogIntegrity(); // Then run every hour setInterval(() => { this.verifyLogIntegrity(); }, 60 * 60 * 1000); // 1 hour logger.info('[LogManagement] Hourly integrity checks scheduled'); } /** * Schedule weekly archival (every Sunday at 3 AM) */ scheduleWeeklyArchival() { const scheduleNextArchival = () => { const now = new Date(); const nextSunday3AM = new Date(); nextSunday3AM.setHours(3, 0, 0, 0); // Calculate days until next Sunday (0 = Sunday) const daysUntilSunday = (7 - now.getDay()) % 7 || 7; nextSunday3AM.setDate(nextSunday3AM.getDate() + daysUntilSunday); // If we're past 3 AM on Sunday, wait until next Sunday if (now.getDay() === 0 && now > nextSunday3AM) { nextSunday3AM.setDate(nextSunday3AM.getDate() + 7); } const msUntilArchival = nextSunday3AM - now; setTimeout(async () => { await this.performWeeklyArchival(); scheduleNextArchival(); // Schedule next week }, msUntilArchival); logger.info(`[LogManagement] Weekly archival scheduled for ${nextSunday3AM.toISOString()}`); }; scheduleNextArchival(); } /** * Perform daily log cleanup with archival */ async performDailyCleanup() { try { logger.info('[LogManagement] Starting daily log cleanup...'); // Get retention settings from environment or defaults const auditRetention = parseInt(process.env.AUDIT_LOG_RETENTION) || 90; const aggregatedRetention = parseInt(process.env.AGGREGATED_LOG_RETENTION) || 90; // Archive logs before deletion await this.archiveOldLogs(auditRetention); // Cleanup audit logs const auditDeleted = await SecurityAuditLogger.cleanupOldLogs(auditRetention); logger.info(`[LogManagement] Cleaned up ${auditDeleted} old security audit logs (>${auditRetention} days)`); // Cleanup aggregated logs const aggregatedDeleted = await logAggregator.cleanup(aggregatedRetention); logger.info(`[LogManagement] Cleaned up ${aggregatedDeleted} old aggregated logs (>${aggregatedRetention} days)`); // Cleanup old file logs (keep last 30 days of rotated files) await this.cleanupFileLogRotations(); // Log the cleanup event await SecurityAuditLogger.logSystemEvent('log_cleanup', true, { auditDeleted, aggregatedDeleted, retentionDays: { audit: auditRetention, aggregated: aggregatedRetention } }); logger.info('[LogManagement] Daily log cleanup completed successfully'); } catch (error) { logger.error('[LogManagement] Error during daily cleanup:', error); await SecurityAuditLogger.logSystemEvent('log_cleanup', false, { error: error.message }); } } /** * Archive old logs to compressed files before deletion */ async archiveOldLogs(retentionDays) { try { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); // Query logs that will be deleted const logsToArchive = await logAggregator.query({ endDate: cutoffDate.toISOString(), limit: 10000 }); if (logsToArchive.length === 0) { logger.info('[LogManagement] No logs to archive'); return; } // Create archive filename const archiveFilename = `logs-archive-${cutoffDate.toISOString().split('T')[0]}-${Date.now()}.json.gz`; const archivePath = path.join(this.archiveDir, archiveFilename); // Compress and save const logsJson = JSON.stringify(logsToArchive, null, 2); const compressed = await gzip(logsJson); await fs.writeFile(archivePath, compressed, { mode: 0o600 }); // Set restrictive permissions await fs.chmod(archivePath, 0o600); logger.info(`[LogManagement] Archived ${logsToArchive.length} logs to ${archiveFilename} (${(compressed.length / 1024).toFixed(2)} KB)`); return archiveFilename; } catch (error) { logger.error('[LogManagement] Error archiving logs:', error); throw error; } } /** * Perform weekly full archival */ async performWeeklyArchival() { try { logger.info('[LogManagement] Starting weekly full log archival...'); // Archive all logs from last week const lastWeek = new Date(); lastWeek.setDate(lastWeek.getDate() - 7); const allLogs = await logAggregator.query({ startDate: new Date(lastWeek.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), endDate: lastWeek.toISOString(), limit: 50000 }); if (allLogs.length > 0) { const archiveFilename = `logs-weekly-${lastWeek.toISOString().split('T')[0]}.json.gz`; const archivePath = path.join(this.archiveDir, archiveFilename); const logsJson = JSON.stringify(allLogs, null, 2); const compressed = await gzip(logsJson); await fs.writeFile(archivePath, compressed, { mode: 0o600 }); logger.info(`[LogManagement] Weekly archive complete: ${archiveFilename} (${allLogs.length} logs, ${(compressed.length / 1024 / 1024).toFixed(2)} MB)`); } // Cleanup old archives (keep 1 year) await this.cleanupOldArchives(365); await SecurityAuditLogger.logSystemEvent('log_weekly_archive', true, { logsArchived: allLogs.length }); } catch (error) { logger.error('[LogManagement] Error during weekly archival:', error); await SecurityAuditLogger.logSystemEvent('log_weekly_archive', false, { error: error.message }); } } /** * Verify log integrity and alert on tampering */ async verifyLogIntegrity() { try { logger.debug('[LogManagement] Starting log integrity verification...'); const result = await logAggregator.verifyIntegrity(); if (result.tampered > 0) { // CRITICAL: Log tampering detected! logger.error(`[LogManagement] ⚠️ LOG TAMPERING DETECTED! ${result.tampered} tampered logs found`); // Log to security audit await SecurityAuditLogger.logSecurityIncident('log_tampering', { tamperedCount: result.tampered, verifiedCount: result.verified, totalCount: result.total, tamperedLogs: result.tamperedLogs.slice(0, 10) // First 10 for details }); // In production, this should trigger alerts (email, Slack, PagerDuty, etc.) logger.error('[LogManagement] Security team should be notified immediately'); } else { logger.debug(`[LogManagement] Integrity check passed: ${result.verified} logs verified`); } return result; } catch (error) { logger.error('[LogManagement] Error during integrity verification:', error); return null; } } /** * Cleanup rotated file logs older than X days */ async cleanupFileLogRotations() { try { const logsDir = path.join(__dirname, '../../logs'); const files = await fs.readdir(logsDir); const now = Date.now(); const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days let deletedCount = 0; for (const file of files) { // Only process rotated files (*.log.1, *.log.2, etc.) if (file.match(/\.log\.\d+$/)) { const filePath = path.join(logsDir, file); const stats = await fs.stat(filePath); const fileAge = now - stats.mtime.getTime(); if (fileAge > maxAge) { await fs.unlink(filePath); deletedCount++; logger.debug(`[LogManagement] Deleted old rotated log: ${file}`); } } } if (deletedCount > 0) { logger.info(`[LogManagement] Cleaned up ${deletedCount} old rotated log files`); } } catch (error) { logger.error('[LogManagement] Error cleaning up rotated logs:', error); } } /** * Cleanup old archives (keep for specified days) */ async cleanupOldArchives(retentionDays) { try { const files = await fs.readdir(this.archiveDir); const now = Date.now(); const maxAge = retentionDays * 24 * 60 * 60 * 1000; let deletedCount = 0; for (const file of files) { if (file.endsWith('.json.gz')) { const filePath = path.join(this.archiveDir, file); const stats = await fs.stat(filePath); const fileAge = now - stats.mtime.getTime(); if (fileAge > maxAge) { await fs.unlink(filePath); deletedCount++; logger.debug(`[LogManagement] Deleted old archive: ${file}`); } } } if (deletedCount > 0) { logger.info(`[LogManagement] Cleaned up ${deletedCount} old log archives`); } } catch (error) { logger.error('[LogManagement] Error cleaning up old archives:', error); } } /** * Get log management statistics */ async getStatistics() { try { const stats = await logAggregator.getStatistics(30); // Get archive info const archives = await fs.readdir(this.archiveDir); const archiveFiles = archives.filter(f => f.endsWith('.json.gz')); let totalArchiveSize = 0; for (const file of archiveFiles) { const filePath = path.join(this.archiveDir, file); const stat = await fs.stat(filePath); totalArchiveSize += stat.size; } return { ...stats, archives: { count: archiveFiles.length, totalSize: totalArchiveSize, totalSizeMB: (totalArchiveSize / 1024 / 1024).toFixed(2) } }; } catch (error) { logger.error('[LogManagement] Error getting statistics:', error); return null; } } /** * List available log archives */ async listArchives() { try { const files = await fs.readdir(this.archiveDir); const archives = []; for (const file of files) { if (file.endsWith('.json.gz')) { const filePath = path.join(this.archiveDir, file); const stats = await fs.stat(filePath); archives.push({ filename: file, size: stats.size, sizeMB: (stats.size / 1024 / 1024).toFixed(2), created: stats.mtime.toISOString() }); } } // Sort by creation date descending archives.sort((a, b) => new Date(b.created) - new Date(a.created)); return archives; } catch (error) { logger.error('[LogManagement] Error listing archives:', error); return []; } } /** * Manual trigger for log cleanup (admin function) */ async manualCleanup(retentionDays) { logger.info(`[LogManagement] Manual cleanup triggered (retention: ${retentionDays} days)`); const auditDeleted = await SecurityAuditLogger.cleanupOldLogs(retentionDays); const aggregatedDeleted = await logAggregator.cleanup(retentionDays); await SecurityAuditLogger.logAdminActivity(null, 'manual_log_cleanup', { auditDeleted, aggregatedDeleted, retentionDays }); return { auditDeleted, aggregatedDeleted }; } /** * Manual trigger for integrity verification (admin function) */ async manualIntegrityCheck() { logger.info('[LogManagement] Manual integrity check triggered'); return await this.verifyLogIntegrity(); } } // Create singleton instance const logManagement = new LogManagement(); module.exports = logManagement;