/* * Copyright 2025 John G Stroh * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Audit Controller * Serves audit logs from MemoryProxy for analytics dashboard */ const fs = require('fs').promises; const path = require('path'); const logger = require('../utils/logger.util'); // Default cost factors (user-configurable) const DEFAULT_COST_FACTORS = { CRITICAL: { amount: 50000, currency: 'USD', rationale: 'Average cost of critical security incident' }, HIGH: { amount: 10000, currency: 'USD', rationale: 'Average cost of high-impact client-facing error' }, MEDIUM: { amount: 2000, currency: 'USD', rationale: 'Average cost of medium-impact issue requiring hotfix' }, LOW: { amount: 500, currency: 'USD', rationale: 'Average developer time cost to fix low-impact issue' } }; let userCostFactors = { ...DEFAULT_COST_FACTORS }; /** * Get audit logs for analytics * GET /api/admin/audit-logs?days=7&environment=production */ async function getAuditLogs(req, res) { try { const { days = 7, limit = 10000, environment } = req.query; // Calculate date range const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - parseInt(days)); // Build query const query = { timestamp: { $gte: startDate } }; // Add environment filter if specified if (environment && environment !== 'all') { query.environment = environment; } // Read from MongoDB instead of JSONL files const db = require('../utils/db.util'); const collection = await db.getCollection('auditLogs'); const decisions = await collection .find(query) .sort({ timestamp: -1 }) .limit(parseInt(limit)) .toArray(); res.json({ success: true, decisions, total: decisions.length, limited: decisions.length, environment: environment || 'all', dateRange: { start: startDate.toISOString(), end: today.toISOString() } }); } catch (error) { logger.error('Error fetching audit logs:', error); res.status(500).json({ success: false, error: error.message }); } } /** * Get audit analytics summary * GET /api/admin/audit-analytics */ async function getAuditAnalytics(req, res) { try { const { days = 7 } = req.query; // Get audit logs const auditLogsResponse = await getAuditLogs(req, { json: data => data }); const decisions = auditLogsResponse.decisions; // Calculate analytics const blockedDecisions = decisions.filter(d => !d.allowed); const allowedDecisions = decisions.filter(d => d.allowed); const analytics = { total: decisions.length, allowed: allowedDecisions.length, blocked: blockedDecisions.length, violations: decisions.filter(d => d.violations && d.violations.length > 0).length, // Block metrics blockRate: decisions.length > 0 ? ((blockedDecisions.length / decisions.length) * 100).toFixed(1) : 0, allowedRate: decisions.length > 0 ? ((allowedDecisions.length / decisions.length) * 100).toFixed(1) : 0, // Block breakdown blocksByReason: {}, blocksBySeverity: {}, frameworkSaves: [], // High severity blocks byAction: {}, byService: {}, bySession: {}, byDate: {}, timeline: [], topViolations: [], // Business intelligence metrics byActivityType: {}, byRiskLevel: {}, byStakeholderImpact: {}, riskHeatmap: [], businessImpactTotal: 0, roiProjections: {}, dateRange: auditLogsResponse.dateRange }; // Group by action decisions.forEach(d => { const action = d.action || 'unknown'; analytics.byAction[action] = (analytics.byAction[action] || 0) + 1; }); // Group by service decisions.forEach(d => { const service = d.service || 'unknown'; analytics.byService[service] = (analytics.byService[service] || 0) + 1; }); // Group by session decisions.forEach(d => { const session = d.sessionId || 'unknown'; analytics.bySession[session] = (analytics.bySession[session] || 0) + 1; }); // Group by date decisions.forEach(d => { const date = new Date(d.timestamp).toISOString().split('T')[0]; analytics.byDate[date] = (analytics.byDate[date] || 0) + 1; }); // Timeline (last 24 hours by hour) const hourCounts = {}; decisions.forEach(d => { const hour = new Date(d.timestamp).getHours(); hourCounts[hour] = (hourCounts[hour] || 0) + 1; }); for (let i = 0; i < 24; i++) { analytics.timeline.push({ hour: i, count: hourCounts[i] || 0 }); } // Analyze blocks by reason and severity blockedDecisions.forEach(d => { // Extract reason from violations or metadata let reason = 'Unknown'; let severity = 'MEDIUM'; if (d.violations && d.violations.length > 0) { // Use first violation's rule ID as reason reason = d.violations[0].ruleId || d.violations[0].ruleText || 'Unknown'; severity = d.violations[0].severity || 'MEDIUM'; // Count by severity analytics.blocksBySeverity[severity] = (analytics.blocksBySeverity[severity] || 0) + 1; // Track high severity blocks as "Framework Saves" if (severity === 'HIGH' || severity === 'CRITICAL') { analytics.frameworkSaves.push({ timestamp: d.timestamp, reason: d.violations[0].ruleText || d.violations[0].details, ruleId: d.violations[0].ruleId, severity: severity, file: d.metadata?.filePath || d.metadata?.file || 'N/A', service: d.service }); } } else if (d.metadata?.reason) { reason = d.metadata.reason; } // Count by reason analytics.blocksByReason[reason] = (analytics.blocksByReason[reason] || 0) + 1; }); // Sort framework saves by timestamp (most recent first) analytics.frameworkSaves.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); analytics.frameworkSaves = analytics.frameworkSaves.slice(0, 10); // Top 10 saves // Top violations (enhanced with more detail) const violationCounts = {}; decisions.forEach(d => { if (d.violations && d.violations.length > 0) { d.violations.forEach(v => { const key = v.ruleId || v.ruleText || 'unknown'; if (!violationCounts[key]) { violationCounts[key] = { ruleId: v.ruleId, ruleText: v.ruleText, severity: v.severity, count: 0 }; } violationCounts[key].count++; }); } }); analytics.topViolations = Object.values(violationCounts) .sort((a, b) => b.count - a.count) .slice(0, 10); // === BUSINESS INTELLIGENCE METRICS === // Activity type breakdown decisions.forEach(d => { const activityType = d.activityType || 'Unknown'; if (!analytics.byActivityType[activityType]) { analytics.byActivityType[activityType] = { total: 0, allowed: 0, blocked: 0, blockRate: 0 }; } analytics.byActivityType[activityType].total++; if (d.allowed) { analytics.byActivityType[activityType].allowed++; } else { analytics.byActivityType[activityType].blocked++; } }); // Calculate block rates for each activity type Object.keys(analytics.byActivityType).forEach(type => { const data = analytics.byActivityType[type]; data.blockRate = data.total > 0 ? ((data.blocked / data.total) * 100).toFixed(1) : '0.0'; }); // Risk level distribution decisions.forEach(d => { const riskLevel = d.riskLevel || 'low'; if (!analytics.byRiskLevel[riskLevel]) { analytics.byRiskLevel[riskLevel] = { total: 0, blocked: 0 }; } analytics.byRiskLevel[riskLevel].total++; if (!d.allowed) { analytics.byRiskLevel[riskLevel].blocked++; } }); // Stakeholder impact distribution decisions.forEach(d => { const impact = d.stakeholderImpact || 'individual'; if (!analytics.byStakeholderImpact[impact]) { analytics.byStakeholderImpact[impact] = { total: 0, blocked: 0, businessImpact: 0 }; } analytics.byStakeholderImpact[impact].total++; if (!d.allowed) { analytics.byStakeholderImpact[impact].blocked++; analytics.byStakeholderImpact[impact].businessImpact += (d.businessImpact || 0); } }); // Risk heatmap: Activity Type vs Risk Level const heatmapData = {}; decisions.forEach(d => { const activityType = d.activityType || 'Unknown'; const riskLevel = d.riskLevel || 'low'; const key = `${activityType}|${riskLevel}`; if (!heatmapData[key]) { heatmapData[key] = { activityType, riskLevel, total: 0, blocked: 0 }; } heatmapData[key].total++; if (!d.allowed) { heatmapData[key].blocked++; } }); analytics.riskHeatmap = Object.values(heatmapData); // Business impact total analytics.businessImpactTotal = blockedDecisions.reduce((sum, d) => sum + (d.businessImpact || 0), 0); // === COST AVOIDANCE CALCULATIONS === // Calculate cost avoided based on user-configured factors let totalCostAvoided = 0; const costBreakdown = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }; blockedDecisions.forEach(d => { if (d.violations && d.violations.length > 0) { d.violations.forEach(v => { const severity = v.severity || 'LOW'; const costFactor = userCostFactors[severity]; if (costFactor) { totalCostAvoided += costFactor.amount; costBreakdown[severity] += costFactor.amount; } }); } }); analytics.costAvoidance = { total: totalCostAvoided, currency: userCostFactors.CRITICAL.currency, breakdown: costBreakdown, costFactors: userCostFactors }; // === AI vs HUMAN PERFORMANCE === // Detect AI vs Human based on service patterns // AI = FileEditHook, Framework services // Human = Manual overrides, direct database operations const aiDecisions = decisions.filter(d => d.service === 'FileEditHook' || d.service === 'BoundaryEnforcer' || d.service === 'ContextPressureMonitor' || d.service === 'MetacognitiveVerifier' ); const humanDecisions = decisions.filter(d => !aiDecisions.includes(d) ); const aiBlocked = aiDecisions.filter(d => !d.allowed).length; const humanBlocked = humanDecisions.filter(d => !d.allowed).length; analytics.teamComparison = { ai: { total: aiDecisions.length, blocked: aiBlocked, blockRate: aiDecisions.length > 0 ? ((aiBlocked / aiDecisions.length) * 100).toFixed(1) : '0.0', label: 'AI Assistant' }, human: { total: humanDecisions.length, blocked: humanBlocked, blockRate: humanDecisions.length > 0 ? ((humanBlocked / humanDecisions.length) * 100).toFixed(1) : '0.0', label: 'Human Direct' } }; // Framework Maturity Score (0-100) // Based on: block rate trend, severity distribution, learning curve const recentDays = 7; const recentDate = new Date(); recentDate.setDate(recentDate.getDate() - recentDays); const recentDecisions = decisions.filter(d => new Date(d.timestamp) >= recentDate); const recentBlocked = recentDecisions.filter(d => !d.allowed).length; const recentBlockRate = recentDecisions.length > 0 ? (recentBlocked / recentDecisions.length) : 0; // Lower block rate = higher maturity (framework teaching good habits) const blockRateScore = Math.max(0, 100 - (recentBlockRate * 1000)); // More AI decisions with low block rate = higher maturity const aiAdoptionScore = aiDecisions.length > 0 ? ((aiDecisions.length / decisions.length) * 100) : 0; // Fewer critical/high severity blocks = higher maturity const criticalCount = blockedDecisions.filter(d => d.violations && d.violations.some(v => v.severity === 'CRITICAL' || v.severity === 'HIGH') ).length; const severityScore = Math.max(0, 100 - (criticalCount / decisions.length * 1000)); analytics.frameworkMaturity = { score: Math.round((blockRateScore + aiAdoptionScore + severityScore) / 3), components: { blockRateScore: Math.round(blockRateScore), aiAdoptionScore: Math.round(aiAdoptionScore), severityScore: Math.round(severityScore) }, trend: recentBlockRate < avgBlockRate ? 'improving' : 'stable', message: recentBlockRate < 0.05 ? 'Excellent - Framework teaching good practices' : recentBlockRate < 0.10 ? 'Good - Team adapting well to governance' : 'Learning - Framework actively preventing violations' }; // ROI Projections const totalDecisions = decisions.length; const totalBlocks = blockedDecisions.length; const avgBlockRate = totalDecisions > 0 ? (totalBlocks / totalDecisions) : 0; analytics.roiProjections = { // Current period metrics decisionsPerDay: days > 0 ? (totalDecisions / days).toFixed(1) : '0', blocksPerDay: days > 0 ? (totalBlocks / days).toFixed(1) : '0', // Projections for scaled deployment projections: [ { userCount: 1000, decisionsPerMonth: Math.round((totalDecisions / days) * 30 * 1000 / decisions.length * 10), // Estimated blocksPerMonth: Math.round((totalBlocks / days) * 30 * 1000 / decisions.length * 10), highSeverityBlocks: Math.round(analytics.frameworkSaves.length / days * 30 * 1000 / decisions.length * 10), businessImpactSaved: Math.round(analytics.businessImpactTotal / days * 30 * 1000 / decisions.length * 10) }, { userCount: 10000, decisionsPerMonth: Math.round((totalDecisions / days) * 30 * 10000 / decisions.length * 10), blocksPerMonth: Math.round((totalBlocks / days) * 30 * 10000 / decisions.length * 10), highSeverityBlocks: Math.round(analytics.frameworkSaves.length / days * 30 * 10000 / decisions.length * 10), businessImpactSaved: Math.round(analytics.businessImpactTotal / days * 30 * 10000 / decisions.length * 10) }, { userCount: 70000, decisionsPerMonth: Math.round((totalDecisions / days) * 30 * 70000 / decisions.length * 10), blocksPerMonth: Math.round((totalBlocks / days) * 30 * 70000 / decisions.length * 10), highSeverityBlocks: Math.round(analytics.frameworkSaves.length / days * 30 * 70000 / decisions.length * 10), businessImpactSaved: Math.round(analytics.businessImpactTotal / days * 30 * 70000 / decisions.length * 10) } ] }; res.json({ success: true, analytics }); } catch (error) { logger.error('Error calculating audit analytics:', error); res.status(500).json({ success: false, error: error.message }); } } /** * Get cost configuration * GET /api/admin/cost-config */ async function getCostConfig(req, res) { try { res.json({ success: true, costFactors: userCostFactors }); } catch (error) { logger.error('Error fetching cost config:', error); res.status(500).json({ success: false, error: error.message }); } } /** * Update cost configuration * POST /api/admin/cost-config */ async function updateCostConfig(req, res) { try { const { costFactors } = req.body; if (!costFactors) { return res.status(400).json({ success: false, error: 'costFactors required' }); } // Validate and update ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].forEach(severity => { if (costFactors[severity]) { userCostFactors[severity] = { amount: parseFloat(costFactors[severity].amount) || userCostFactors[severity].amount, currency: costFactors[severity].currency || userCostFactors[severity].currency, rationale: costFactors[severity].rationale || userCostFactors[severity].rationale }; } }); logger.info('Cost factors updated by', req.user?.username || 'admin'); res.json({ success: true, costFactors: userCostFactors }); } catch (error) { logger.error('Error updating cost config:', error); res.status(500).json({ success: false, error: error.message }); } } /** * Export audit logs for cross-environment research (privacy-preserving) * GET /api/admin/audit-export?since=YYYY-MM-DD */ async function exportAuditLogs(req, res) { try { const { since } = req.query; if (!since) { return res.status(400).json({ success: false, error: 'since parameter required (ISO date format)' }); } // Parse date const sinceDate = new Date(since); if (isNaN(sinceDate.getTime())) { return res.status(400).json({ success: false, error: 'Invalid date format for since parameter' }); } // Fetch logs since date const logs = await AuditLog.find({ timestamp: { $gte: sinceDate } }).sort({ timestamp: 1 }).lean(); // Sanitize for privacy const { sanitizeBatch } = require('../utils/audit-sanitizer.util'); const sanitized = sanitizeBatch(logs); logger.info(`Exported ${sanitized.length} audit logs since ${since} for ${req.user?.username || 'unknown'}`); res.json({ success: true, count: sanitized.length, since: sinceDate, exported_at: new Date(), logs: sanitized }); } catch (error) { logger.error('Error exporting audit logs:', error); res.status(500).json({ success: false, error: error.message }); } } module.exports = { getAuditLogs, getAuditAnalytics, getCostConfig, updateCostConfig, exportAuditLogs };