219 lines
6.4 KiB
JavaScript
219 lines
6.4 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const router = express.Router();
|
||
|
|
const { authenticate, requireAdmin } = require('../middleware/auth');
|
||
|
|
const logger = require('../utils/logger');
|
||
|
|
const { db } = require('../database/db');
|
||
|
|
|
||
|
|
// Store CSP violations in database
|
||
|
|
db.run(`
|
||
|
|
CREATE TABLE IF NOT EXISTS csp_violations (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
document_uri TEXT,
|
||
|
|
violated_directive TEXT,
|
||
|
|
blocked_uri TEXT,
|
||
|
|
source_file TEXT,
|
||
|
|
line_number INTEGER,
|
||
|
|
column_number INTEGER,
|
||
|
|
user_agent TEXT,
|
||
|
|
ip_address TEXT,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
|
|
)
|
||
|
|
`);
|
||
|
|
|
||
|
|
// CSP violation reporting endpoint (no auth required)
|
||
|
|
router.post('/report', express.json({ type: 'application/csp-report' }), (req, res) => {
|
||
|
|
const report = req.body['csp-report'];
|
||
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||
|
|
const userAgent = req.headers['user-agent'];
|
||
|
|
|
||
|
|
if (!report) {
|
||
|
|
return res.status(400).json({ error: 'Invalid CSP report' });
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.warn('CSP Violation:', {
|
||
|
|
documentUri: report['document-uri'],
|
||
|
|
violatedDirective: report['violated-directive'],
|
||
|
|
blockedUri: report['blocked-uri'],
|
||
|
|
sourceFile: report['source-file'],
|
||
|
|
lineNumber: report['line-number'],
|
||
|
|
columnNumber: report['column-number'],
|
||
|
|
ip,
|
||
|
|
userAgent
|
||
|
|
});
|
||
|
|
|
||
|
|
// Store in database
|
||
|
|
db.run(
|
||
|
|
`INSERT INTO csp_violations
|
||
|
|
(document_uri, violated_directive, blocked_uri, source_file, line_number, column_number, user_agent, ip_address)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
|
|
[
|
||
|
|
report['document-uri'],
|
||
|
|
report['violated-directive'],
|
||
|
|
report['blocked-uri'],
|
||
|
|
report['source-file'],
|
||
|
|
report['line-number'],
|
||
|
|
report['column-number'],
|
||
|
|
userAgent,
|
||
|
|
ip
|
||
|
|
],
|
||
|
|
(err) => {
|
||
|
|
if (err) {
|
||
|
|
logger.error('Failed to store CSP violation:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
res.status(204).end();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get CSP violations (admin only)
|
||
|
|
router.get('/violations', authenticate, requireAdmin, (req, res) => {
|
||
|
|
const { limit = 100, offset = 0 } = req.query;
|
||
|
|
|
||
|
|
db.all(
|
||
|
|
`SELECT * FROM csp_violations
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT ? OFFSET ?`,
|
||
|
|
[parseInt(limit), parseInt(offset)],
|
||
|
|
(err, violations) => {
|
||
|
|
if (err) {
|
||
|
|
logger.error('Failed to fetch CSP violations:', err);
|
||
|
|
return res.status(500).json({ error: 'Failed to fetch violations' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get total count
|
||
|
|
db.get('SELECT COUNT(*) as total FROM csp_violations', (countErr, countResult) => {
|
||
|
|
if (countErr) {
|
||
|
|
logger.error('Failed to count CSP violations:', countErr);
|
||
|
|
return res.status(500).json({ error: 'Failed to count violations' });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
violations,
|
||
|
|
total: countResult.total,
|
||
|
|
limit: parseInt(limit),
|
||
|
|
offset: parseInt(offset)
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get CSP violation statistics (admin only)
|
||
|
|
router.get('/stats', authenticate, requireAdmin, (req, res) => {
|
||
|
|
const { days = 7 } = req.query;
|
||
|
|
const cutoffDate = new Date();
|
||
|
|
cutoffDate.setDate(cutoffDate.getDate() - parseInt(days));
|
||
|
|
|
||
|
|
Promise.all([
|
||
|
|
// Total violations
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
db.get(
|
||
|
|
'SELECT COUNT(*) as total FROM csp_violations WHERE created_at >= ?',
|
||
|
|
[cutoffDate.toISOString()],
|
||
|
|
(err, row) => err ? reject(err) : resolve(row.total)
|
||
|
|
);
|
||
|
|
}),
|
||
|
|
// By directive
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT violated_directive, COUNT(*) as count
|
||
|
|
FROM csp_violations
|
||
|
|
WHERE created_at >= ?
|
||
|
|
GROUP BY violated_directive
|
||
|
|
ORDER BY count DESC
|
||
|
|
LIMIT 10`,
|
||
|
|
[cutoffDate.toISOString()],
|
||
|
|
(err, rows) => err ? reject(err) : resolve(rows)
|
||
|
|
);
|
||
|
|
}),
|
||
|
|
// By blocked URI
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT blocked_uri, COUNT(*) as count
|
||
|
|
FROM csp_violations
|
||
|
|
WHERE created_at >= ?
|
||
|
|
GROUP BY blocked_uri
|
||
|
|
ORDER BY count DESC
|
||
|
|
LIMIT 10`,
|
||
|
|
[cutoffDate.toISOString()],
|
||
|
|
(err, rows) => err ? reject(err) : resolve(rows)
|
||
|
|
);
|
||
|
|
}),
|
||
|
|
// Recent violations
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
db.all(
|
||
|
|
`SELECT * FROM csp_violations
|
||
|
|
WHERE created_at >= ?
|
||
|
|
ORDER BY created_at DESC
|
||
|
|
LIMIT 20`,
|
||
|
|
[cutoffDate.toISOString()],
|
||
|
|
(err, rows) => err ? reject(err) : resolve(rows)
|
||
|
|
);
|
||
|
|
})
|
||
|
|
])
|
||
|
|
.then(([total, byDirective, byUri, recent]) => {
|
||
|
|
res.json({
|
||
|
|
total,
|
||
|
|
byDirective,
|
||
|
|
byUri,
|
||
|
|
recent,
|
||
|
|
days: parseInt(days)
|
||
|
|
});
|
||
|
|
})
|
||
|
|
.catch((err) => {
|
||
|
|
logger.error('Failed to fetch CSP stats:', err);
|
||
|
|
res.status(500).json({ error: 'Failed to fetch statistics' });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Clear old CSP violations (admin only)
|
||
|
|
router.delete('/violations', authenticate, requireAdmin, (req, res) => {
|
||
|
|
const { days = 30 } = req.query;
|
||
|
|
const cutoffDate = new Date();
|
||
|
|
cutoffDate.setDate(cutoffDate.getDate() - parseInt(days));
|
||
|
|
|
||
|
|
db.run(
|
||
|
|
'DELETE FROM csp_violations WHERE created_at < ?',
|
||
|
|
[cutoffDate.toISOString()],
|
||
|
|
function(err) {
|
||
|
|
if (err) {
|
||
|
|
logger.error('Failed to delete old CSP violations:', err);
|
||
|
|
return res.status(500).json({ error: 'Failed to delete violations' });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
message: 'Old violations cleared',
|
||
|
|
deleted: this.changes
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get current CSP policy (authenticated users)
|
||
|
|
router.get('/policy', authenticate, (req, res) => {
|
||
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
mode: isProduction ? 'enforce' : 'report-only',
|
||
|
|
policy: {
|
||
|
|
defaultSrc: ["'self'"],
|
||
|
|
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://www.gstatic.com"],
|
||
|
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||
|
|
fontSrc: ["'self'", "data:", "https://fonts.gstatic.com"],
|
||
|
|
imgSrc: ["'self'", "data:", "blob:", "https:", "http:"],
|
||
|
|
mediaSrc: ["'self'", "blob:", "data:", "mediastream:", "https:", "http:", "*"],
|
||
|
|
connectSrc: ["'self'", "https:", "http:", "ws:", "wss:", "blob:", "*"],
|
||
|
|
frameSrc: ["'self'", "https://www.youtube.com", "https://player.vimeo.com"],
|
||
|
|
objectSrc: ["'none'"],
|
||
|
|
baseUri: ["'self'"],
|
||
|
|
formAction: ["'self'"],
|
||
|
|
frameAncestors: ["'self'"],
|
||
|
|
upgradeInsecureRequests: isProduction
|
||
|
|
},
|
||
|
|
reportUri: '/api/csp/report'
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
module.exports = router;
|