const express = require('express'); const fs = require('fs').promises; module.exports = function({ asyncHandler, ok, auditLogger, securityEventStore }) { const router = express.Router(); // GET /api/v1/log-insights — Plain English summary of who's doing what router.get('/log-insights', asyncHandler(async (req, res) => { const hours = parseInt(req.query.hours) || 24; const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); // --- Collect data --- const auditEntries = await auditLogger.query({ limit: 10000 }); const recentAudit = auditEntries.filter(e => e.timestamp >= since); let securityEvents = []; try { securityEvents = securityEventStore.query({ since, limit: 10000 }); } catch {} // --- Analyze IPs --- const ipMap = {}; recentAudit.forEach(e => { const ip = e.ip || 'unknown'; if (!ipMap[ip]) ipMap[ip] = { count: 0, actions: {}, resources: new Set(), first: e.timestamp, last: e.timestamp, failures: 0 }; const s = ipMap[ip]; s.count++; const cat = (e.action || 'unknown').split('.')[0]; s.actions[cat] = (s.actions[cat] || 0) + 1; if (e.resource) s.resources.add(e.resource); if (e.timestamp < s.first) s.first = e.timestamp; if (e.timestamp > s.last) s.last = e.timestamp; if (e.outcome === 'failure' || e.outcome === 'denied') s.failures++; }); // --- Build plain-English insights --- const insights = []; const ipArray = Object.entries(ipMap).sort((a, b) => b[1].count - a[1].count); // Heavy users ipArray.slice(0, 3).forEach(([ip, s]) => { const topAction = Object.entries(s.actions).sort((a, b) => b[1] - a[1])[0]; insights.push({ severity: s.count > 500 ? 'warning' : 'info', title: ip + ' — ' + s.count + ' requests in ' + hours + 'h', plain: ip + ' made ' + s.count + ' requests (mostly ' + (topAction ? topAction[0] : 'unknown') + ')' + (s.failures > 0 ? ', ' + s.failures + ' failed' : '') + '.' }); }); // Auth failures const totalFailures = recentAudit.filter(e => e.outcome === 'failure' || e.outcome === 'denied').length; if (totalFailures > 5) { insights.push({ severity: totalFailures > 50 ? 'warning' : 'info', title: totalFailures + ' failed actions', plain: totalFailures + ' requests were denied or failed in the last ' + hours + ' hours.' + (totalFailures > 50 ? ' This could indicate someone trying to brute-force access.' : '') }); } // Security events const secBySev = {}; securityEvents.forEach(e => { secBySev[e.severity] = (secBySev[e.severity] || 0) + 1; }); if (secBySev.critical || secBySev.error) { insights.push({ severity: 'warning', title: ((secBySev.critical || 0) + (secBySev.error || 0)) + ' security alerts', plain: (secBySev.critical || 0) + ' critical and ' + (secBySev.error || 0) + ' error-level security events were logged.' }); } // Quiet / nothing if (insights.length === 0) { insights.push({ severity: 'ok', title: 'All quiet', plain: 'No notable activity in the last ' + hours + ' hours.' }); } // --- Storage info --- const auditPath = process.env.AUDIT_LOG_FILE || '/opt/dashcaddy/dashcaddy-api/data/audit-log.json'; const secPath = process.env.SECURITY_EVENT_LOG_FILE || '/opt/dashcaddy/dashcaddy-api/data/security-events.jsonl'; let storage = {}; try { const a = await fs.stat(auditPath); storage.auditLog = { sizeMB: +(a.size / 1048576).toFixed(2), entries: auditEntries.length }; } catch {} try { const s = await fs.stat(secPath); storage.securityEvents = { sizeMB: +(s.size / 1048576).toFixed(2), entries: securityEvents.length }; } catch {} ok(res, { period: { hours, since, until: new Date().toISOString() }, summary: { totalRequests: recentAudit.length, uniqueIPs: ipArray.length, securityEvents: securityEvents.length, failedActions: totalFailures }, topIPs: ipArray.slice(0, 10).map(([ip, s]) => ({ ip: ip, count: s.count, failures: s.failures, topActions: Object.entries(s.actions).sort((a, b) => b[1] - a[1]).slice(0, 3), activeFrom: s.first, lastSeen: s.last })), insights: insights, storage: storage }); })); // POST /api/v1/log-insights/dispose — Preview then confirm cleanup router.post('/log-insights/dispose', asyncHandler(async (req, res) => { const keepDays = parseInt(req.body.keepDays) || 30; const confirm = req.body.confirm === true; const cutoff = new Date(Date.now() - keepDays * 86400000).toISOString(); const auditPath = process.env.AUDIT_LOG_FILE || '/opt/dashcaddy/dashcaddy-api/data/audit-log.json'; const secPath = process.env.SECURITY_EVENT_LOG_FILE || '/opt/dashcaddy/dashcaddy-api/data/security-events.jsonl'; const auditRaw = await fs.readFile(auditPath, 'utf8').catch(function () { return '[]'; }); const auditData = JSON.parse(auditRaw); const oldAudit = auditData.filter(function (e) { return e.timestamp < cutoff; }); const secRaw = await fs.readFile(secPath, 'utf8').catch(function () { return ''; }); const secLines = secRaw.split('\n').filter(Boolean); const oldSec = secLines.filter(function (l) { try { return JSON.parse(l).timestamp < cutoff; } catch (e) { return false; } }); if (!confirm) { ok(res, { preview: true, message: 'This will delete ' + oldAudit.length + ' audit entries and ' + oldSec.length + ' security events older than ' + keepDays + ' days. Send {confirm: true} to proceed.', wouldDelete: { auditEntries: oldAudit.length, securityEvents: oldSec.length }, cutoffDate: cutoff }); return; } // Execute cleanup const keptAudit = auditData.filter(function (e) { return e.timestamp >= cutoff; }); await fs.writeFile(auditPath, JSON.stringify(keptAudit, null, 2)); const keptSec = secLines.filter(function (l) { try { return JSON.parse(l).timestamp >= cutoff; } catch (e) { return false; } }); await fs.writeFile(secPath, keptSec.join('\n') + '\n'); ok(res, { disposed: true, deleted: { auditEntries: oldAudit.length, securityEvents: oldSec.length }, remaining: { auditEntries: keptAudit.length, securityEvents: keptSec.length }, cutoffDate: cutoff }); })); return router; };