479 lines
14 KiB
JavaScript
479 lines
14 KiB
JavaScript
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;
|