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;