const express = require('express'); const router = express.Router(); const { authenticate } = require('../middleware/auth'); const { requirePermission } = require('../middleware/rbac'); const { readLimiter } = require('../middleware/rateLimiter'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const fs = require('fs').promises; const path = require('path'); const { exec } = require('child_process'); const { promisify } = require('util'); const execPromise = promisify(exec); /** * Security Monitoring & Dependency Management * Provides comprehensive security status and vulnerability tracking */ // Get comprehensive security status router.get('/status', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { const securityStatus = { timestamp: new Date().toISOString(), dependencies: await checkDependencies(), vulnerabilities: await checkVulnerabilities(), securityHeaders: await checkSecurityHeaders(), auditSummary: await getAuditSummary(), systemHealth: await getSystemHealth() }; res.json(securityStatus); } catch (error) { logger.error('Error fetching security status:', error); res.status(500).json({ error: 'Failed to fetch security status' }); } }); // Check dependencies for updates async function checkDependencies() { try { const backendPackage = JSON.parse( await fs.readFile(path.join(__dirname, '../package.json'), 'utf8') ); const frontendPackage = JSON.parse( await fs.readFile(path.join(__dirname, '../../frontend/package.json'), 'utf8') ); return { backend: { dependencies: Object.keys(backendPackage.dependencies || {}).length, devDependencies: Object.keys(backendPackage.devDependencies || {}).length, lastChecked: new Date().toISOString() }, frontend: { dependencies: Object.keys(frontendPackage.dependencies || {}).length, devDependencies: Object.keys(frontendPackage.devDependencies || {}).length, lastChecked: new Date().toISOString() } }; } catch (error) { logger.error('Error checking dependencies:', error); return { error: 'Unable to check dependencies' }; } } // Check for known vulnerabilities async function checkVulnerabilities() { try { // Run npm audit in both backend and frontend const backendAudit = await runNpmAudit('backend'); const frontendAudit = await runNpmAudit('frontend'); return { backend: backendAudit, frontend: frontendAudit, lastScanned: new Date().toISOString() }; } catch (error) { logger.error('Error checking vulnerabilities:', error); return { error: 'Unable to scan for vulnerabilities' }; } } async function runNpmAudit(project) { try { const projectPath = project === 'backend' ? path.join(__dirname, '..') : path.join(__dirname, '../../frontend'); const { stdout } = await execPromise(`cd ${projectPath} && npm audit --json`, { timeout: 30000 }); const auditData = JSON.parse(stdout); return { total: auditData.metadata?.vulnerabilities?.total || 0, critical: auditData.metadata?.vulnerabilities?.critical || 0, high: auditData.metadata?.vulnerabilities?.high || 0, moderate: auditData.metadata?.vulnerabilities?.moderate || 0, low: auditData.metadata?.vulnerabilities?.low || 0, info: auditData.metadata?.vulnerabilities?.info || 0 }; } catch (error) { // npm audit returns non-zero exit code when vulnerabilities are found if (error.stdout) { try { const auditData = JSON.parse(error.stdout); return { total: auditData.metadata?.vulnerabilities?.total || 0, critical: auditData.metadata?.vulnerabilities?.critical || 0, high: auditData.metadata?.vulnerabilities?.high || 0, moderate: auditData.metadata?.vulnerabilities?.moderate || 0, low: auditData.metadata?.vulnerabilities?.low || 0, info: auditData.metadata?.vulnerabilities?.info || 0 }; } catch { return { error: 'Unable to parse audit results' }; } } return { error: error.message }; } } // Check security headers configuration async function checkSecurityHeaders() { return { helmet: { enabled: true, features: [ 'Content-Security-Policy', 'X-Content-Type-Options', 'X-Frame-Options', 'X-XSS-Protection', 'Strict-Transport-Security', 'Referrer-Policy' ] }, csp: { enabled: true, mode: process.env.NODE_ENV === 'production' ? 'enforcing' : 'report-only' }, cors: { enabled: true } }; } // Get security audit summary async function getAuditSummary() { return new Promise((resolve, reject) => { db.all( `SELECT action, result, COUNT(*) as count, MAX(timestamp) as last_occurrence FROM security_audit_log WHERE timestamp > datetime('now', '-7 days') GROUP BY action, result ORDER BY count DESC LIMIT 20`, [], (err, rows) => { if (err) { logger.error('Error fetching audit summary:', err); resolve({ error: 'Unable to fetch audit summary' }); } else { resolve(rows || []); } } ); }); } // Get system health metrics async function getSystemHealth() { return new Promise((resolve, reject) => { Promise.all([ // Active sessions count new Promise((res) => { db.get('SELECT COUNT(*) as count FROM sessions WHERE expires_at > ?', [new Date().toISOString()], (err, row) => res(row?.count || 0) ); }), // Failed login attempts in last hour new Promise((res) => { db.get( `SELECT COUNT(*) as count FROM security_audit_log WHERE action = 'login' AND result = 'failed' AND timestamp > datetime('now', '-1 hour')`, [], (err, row) => res(row?.count || 0) ); }), // Locked accounts new Promise((res) => { db.get( 'SELECT COUNT(*) as count FROM users WHERE locked_until > ?', [new Date().toISOString()], (err, row) => res(row?.count || 0) ); }), // Total users new Promise((res) => { db.get('SELECT COUNT(*) as count FROM users', [], (err, row) => res(row?.count || 0)); }) ]).then(([activeSessions, failedLogins, lockedAccounts, totalUsers]) => { resolve({ activeSessions, failedLogins, lockedAccounts, totalUsers, timestamp: new Date().toISOString() }); }); }); } // Get detailed vulnerability report router.get('/vulnerabilities/detailed', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { const backendPath = path.join(__dirname, '..'); const frontendPath = path.join(__dirname, '../../frontend'); const backendAudit = await execPromise(`cd ${backendPath} && npm audit --json`, { timeout: 30000 }).catch(e => ({ stdout: e.stdout })); const frontendAudit = await execPromise(`cd ${frontendPath} && npm audit --json`, { timeout: 30000 }).catch(e => ({ stdout: e.stdout })); const backendData = JSON.parse(backendAudit.stdout || '{}'); const frontendData = JSON.parse(frontendAudit.stdout || '{}'); res.json({ backend: { vulnerabilities: backendData.vulnerabilities || {}, metadata: backendData.metadata || {} }, frontend: { vulnerabilities: frontendData.vulnerabilities || {}, metadata: frontendData.metadata || {} }, timestamp: new Date().toISOString() }); } catch (error) { logger.error('Error fetching detailed vulnerabilities:', error); res.status(500).json({ error: 'Failed to fetch vulnerability details' }); } }); // Get security audit log with filtering router.get('/audit-log', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { const { action, result, userId, startDate, endDate, limit = 100, offset = 0 } = req.query; let query = 'SELECT * FROM security_audit_log WHERE 1=1'; const params = []; if (action) { query += ' AND action = ?'; params.push(action); } if (result) { query += ' AND result = ?'; params.push(result); } if (userId) { query += ' AND user_id = ?'; params.push(userId); } if (startDate) { query += ' AND timestamp >= ?'; params.push(startDate); } if (endDate) { query += ' AND timestamp <= ?'; params.push(endDate); } query += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); db.all(query, params, (err, rows) => { if (err) { logger.error('Error fetching audit log:', err); return res.status(500).json({ error: 'Failed to fetch audit log' }); } // Get total count for pagination let countQuery = 'SELECT COUNT(*) as total FROM security_audit_log WHERE 1=1'; const countParams = params.slice(0, -2); // Remove limit and offset db.get(countQuery, countParams, (err, countRow) => { if (err) { logger.error('Error counting audit log:', err); return res.json({ logs: rows || [], total: 0 }); } res.json({ logs: rows || [], total: countRow?.total || 0, limit: parseInt(limit), offset: parseInt(offset) }); }); }); } catch (error) { logger.error('Error fetching audit log:', error); res.status(500).json({ error: 'Failed to fetch audit log' }); } }); // Export audit log router.get('/audit-log/export', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { const { format = 'json', startDate, endDate } = req.query; let query = 'SELECT * FROM security_audit_log WHERE 1=1'; const params = []; if (startDate) { query += ' AND timestamp >= ?'; params.push(startDate); } if (endDate) { query += ' AND timestamp <= ?'; params.push(endDate); } query += ' ORDER BY timestamp DESC'; db.all(query, params, (err, rows) => { if (err) { logger.error('Error exporting audit log:', err); return res.status(500).json({ error: 'Failed to export audit log' }); } if (format === 'csv') { const csv = convertToCSV(rows); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename=security-audit-${Date.now()}.csv`); res.send(csv); } else { res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Disposition', `attachment; filename=security-audit-${Date.now()}.json`); res.json(rows); } }); } catch (error) { logger.error('Error exporting audit log:', error); res.status(500).json({ error: 'Failed to export audit log' }); } }); function convertToCSV(data) { if (!data || data.length === 0) return ''; const headers = Object.keys(data[0]); const csvRows = [headers.join(',')]; for (const row of data) { const values = headers.map(header => { const value = row[header]; return typeof value === 'string' && value.includes(',') ? `"${value}"` : value; }); csvRows.push(values.join(',')); } return csvRows.join('\n'); } // Get security recommendations router.get('/recommendations', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { const recommendations = []; // Check for locked accounts const lockedAccounts = await new Promise((resolve) => { db.get( 'SELECT COUNT(*) as count FROM users WHERE locked_until > ?', [new Date().toISOString()], (err, row) => resolve(row?.count || 0) ); }); if (lockedAccounts > 0) { recommendations.push({ severity: 'warning', category: 'account_security', title: 'Locked Accounts', description: `${lockedAccounts} account(s) are currently locked due to failed login attempts`, action: 'Review locked accounts and consider unlocking legitimate users' }); } // Check for users with old passwords const oldPasswords = await new Promise((resolve) => { db.all( `SELECT username, password_changed_at FROM users WHERE password_changed_at < datetime('now', '-90 days')`, [], (err, rows) => resolve(rows || []) ); }); if (oldPasswords.length > 0) { recommendations.push({ severity: 'info', category: 'password_policy', title: 'Old Passwords', description: `${oldPasswords.length} user(s) haven't changed their password in over 90 days`, action: 'Encourage users to update their passwords regularly' }); } // Check for recent failed logins const recentFailures = await new Promise((resolve) => { db.get( `SELECT COUNT(*) as count FROM security_audit_log WHERE action = 'login' AND result = 'failed' AND timestamp > datetime('now', '-1 hour')`, [], (err, row) => resolve(row?.count || 0) ); }); if (recentFailures > 10) { recommendations.push({ severity: 'high', category: 'threat_detection', title: 'High Failed Login Rate', description: `${recentFailures} failed login attempts in the last hour`, action: 'Investigate potential brute-force attack' }); } // Check for users without 2FA const no2FA = await new Promise((resolve) => { db.get( 'SELECT COUNT(*) as count FROM users WHERE two_factor_secret IS NULL', [], (err, row) => resolve(row?.count || 0) ); }); if (no2FA > 0) { recommendations.push({ severity: 'warning', category: 'authentication', title: 'Two-Factor Authentication', description: `${no2FA} user(s) don't have 2FA enabled`, action: 'Encourage users to enable two-factor authentication' }); } res.json({ recommendations, timestamp: new Date().toISOString() }); } catch (error) { logger.error('Error generating recommendations:', error); res.status(500).json({ error: 'Failed to generate recommendations' }); } }); module.exports = router;