/** * Settings Audit Trail * Log semua perubahan settings untuk compliance & debugging */ const fs = require('fs'); const path = require('path'); const { logger } = require('./logger'); const { isSensitiveField, maskValue } = require('./settingsEncryption'); const AUDIT_LOG_DIR = path.join(__dirname, '../logs/settings-audit'); const AUDIT_LOG_FILE = path.join(AUDIT_LOG_DIR, 'settings-changes.jsonl'); // Ensure audit log directory exists if (!fs.existsSync(AUDIT_LOG_DIR)) { fs.mkdirSync(AUDIT_LOG_DIR, { recursive: true }); } /** * Log settings change */ function logSettingsChange(actor, changes, metadata = {}) { try { const timestamp = new Date().toISOString(); // Mask sensitive values const maskedChanges = {}; Object.keys(changes).forEach(field => { if (isSensitiveField(field)) { maskedChanges[field] = { oldValue: maskValue(changes[field].oldValue), newValue: maskValue(changes[field].newValue) }; } else { maskedChanges[field] = changes[field]; } }); const logEntry = { timestamp, actor: actor || 'system', changes: maskedChanges, ip: metadata.ip || 'unknown', userAgent: metadata.userAgent || 'unknown', metadata }; // Write to JSONL file fs.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(logEntry) + '\n', 'utf-8'); logger.info(`[settings-audit] Settings changed by ${actor}`, { changes: Object.keys(maskedChanges), timestamp }); return true; } catch (error) { logger.error(`[settings-audit] Error logging settings change: ${error.message}`); return false; } } /** * Get settings change history */ function getChangeHistory(limit = 100) { try { if (!fs.existsSync(AUDIT_LOG_FILE)) return []; const lines = fs.readFileSync(AUDIT_LOG_FILE, 'utf-8').split('\n').filter(l => l.trim()); const entries = lines.map(line => JSON.parse(line)); // Return latest entries return entries.slice(-limit).reverse(); } catch (error) { logger.error(`[settings-audit] Error reading change history: ${error.message}`); return []; } } /** * Get changes by actor */ function getChangesByActor(actor, limit = 50) { try { const history = getChangeHistory(1000); return history.filter(entry => entry.actor === actor).slice(0, limit); } catch (error) { logger.error(`[settings-audit] Error filtering by actor: ${error.message}`); return []; } } /** * Get changes by field */ function getChangesByField(field, limit = 50) { try { const history = getChangeHistory(1000); return history.filter(entry => entry.changes[field]).slice(0, limit); } catch (error) { logger.error(`[settings-audit] Error filtering by field: ${error.message}`); return []; } } /** * Get changes in date range */ function getChangesByDateRange(startDate, endDate, limit = 100) { try { const history = getChangeHistory(1000); const start = new Date(startDate).getTime(); const end = new Date(endDate).getTime(); return history.filter(entry => { const entryTime = new Date(entry.timestamp).getTime(); return entryTime >= start && entryTime <= end; }).slice(0, limit); } catch (error) { logger.error(`[settings-audit] Error filtering by date range: ${error.message}`); return []; } } /** * Export audit log */ function exportAuditLog(format = 'json') { try { const history = getChangeHistory(10000); if (format === 'csv') { // Convert to CSV const headers = ['Timestamp', 'Actor', 'Field', 'Old Value', 'New Value', 'IP']; const rows = []; history.forEach(entry => { Object.keys(entry.changes).forEach(field => { rows.push([ entry.timestamp, entry.actor, field, entry.changes[field].oldValue, entry.changes[field].newValue, entry.ip ]); }); }); return { headers, rows, data: [headers, ...rows].map(row => row.join(',')).join('\n') }; } // Default JSON format return { format: 'json', count: history.length, data: history }; } catch (error) { logger.error(`[settings-audit] Error exporting audit log: ${error.message}`); return null; } } /** * Clear old audit logs (retention policy) */ function clearOldLogs(daysToKeep = 90) { try { if (!fs.existsSync(AUDIT_LOG_FILE)) return; const lines = fs.readFileSync(AUDIT_LOG_FILE, 'utf-8').split('\n').filter(l => l.trim()); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); const recentLines = lines.filter(line => { try { const entry = JSON.parse(line); return new Date(entry.timestamp) > cutoffDate; } catch { return false; } }); fs.writeFileSync(AUDIT_LOG_FILE, recentLines.join('\n') + '\n', 'utf-8'); logger.info(`[settings-audit] Cleared old logs, kept ${recentLines.length} entries`); return true; } catch (error) { logger.error(`[settings-audit] Error clearing old logs: ${error.message}`); return false; } } /** * Get audit statistics */ function getAuditStats() { try { const history = getChangeHistory(10000); const stats = { totalChanges: history.length, uniqueActors: new Set(history.map(e => e.actor)).size, changedFields: new Set(), changesByActor: {}, changesByField: {}, lastChange: history[0] || null }; history.forEach(entry => { // Count by actor stats.changesByActor[entry.actor] = (stats.changesByActor[entry.actor] || 0) + 1; // Count by field Object.keys(entry.changes).forEach(field => { stats.changedFields.add(field); stats.changesByField[field] = (stats.changesByField[field] || 0) + 1; }); }); stats.changedFields = Array.from(stats.changedFields); return stats; } catch (error) { logger.error(`[settings-audit] Error getting audit stats: ${error.message}`); return null; } } module.exports = { logSettingsChange, getChangeHistory, getChangesByActor, getChangesByField, getChangesByDateRange, exportAuditLog, clearOldLogs, getAuditStats, AUDIT_LOG_FILE };