#!/usr/bin/env node /** * CSP Violation Scanner * Scans HTML and JS files for Content Security Policy violations * * Violations checked (inst_008): * - Inline event handlers (onclick, onload, etc.) * - Inline styles (style="...") * - Inline scripts () * - javascript: URLs * * Usage: * node scripts/check-csp-violations.js [pattern] * * Examples: * node scripts/check-csp-violations.js * node scripts/check-csp-violations.js public */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Default patterns to scan const DEFAULT_PATTERNS = [ 'public/**/*.html', 'public/**/*.js' ]; // CSP violation patterns const VIOLATION_PATTERNS = { inline_event_handlers: { regex: /\s(on[a-z]+)=["'][^"']*["']/gi, description: 'Inline event handler', severity: 'HIGH' }, inline_styles: { regex: /\sstyle=["'][^"']*["']/gi, description: 'Inline style attribute', severity: 'HIGH' }, inline_scripts: { regex: /]*\ssrc=)[^>]*>[\s\S]*?<\/script>/gi, description: 'Inline script block', severity: 'CRITICAL' }, javascript_urls: { regex: /href=["']javascript:/gi, description: 'javascript: URL', severity: 'CRITICAL' } }; // Files to exclude from scanning const EXCLUDED_PATTERNS = [ 'node_modules', '.git', 'dist', 'build' ]; /** * Find files matching glob patterns */ function findFiles(patterns) { const allPatterns = Array.isArray(patterns) ? patterns : [patterns]; const files = new Set(); allPatterns.forEach(pattern => { try { const result = execSync(`find public -type f \\( -name "*.html" -o -name "*.js" \\) 2>/dev/null`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }); result.split('\n').filter(f => f.trim()).forEach(file => { // Exclude patterns if (!EXCLUDED_PATTERNS.some(excluded => file.includes(excluded))) { files.add(file); } }); } catch (error) { // Ignore errors from find command } }); return Array.from(files).sort(); } /** * Scan a single file for CSP violations */ function scanFile(filePath) { const violations = []; try { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n'); // Check each violation pattern Object.entries(VIOLATION_PATTERNS).forEach(([type, pattern]) => { const matches = content.matchAll(pattern.regex); for (const match of matches) { // Find line number const beforeMatch = content.substring(0, match.index); const lineNumber = beforeMatch.split('\n').length; // Get the matched text (truncate if too long) let matchedText = match[0]; if (matchedText.length > 80) { matchedText = matchedText.substring(0, 77) + '...'; } violations.push({ file: filePath, line: lineNumber, type, description: pattern.description, severity: pattern.severity, matched: matchedText }); } }); } catch (error) { console.error(`Error scanning ${filePath}:`, error.message); } return violations; } /** * Main scanner function */ function scanForViolations(patterns = DEFAULT_PATTERNS) { console.log('šŸ” Scanning for CSP violations...\n'); const files = findFiles(patterns); console.log(`Found ${files.length} files to scan\n`); const allViolations = []; files.forEach(file => { const violations = scanFile(file); if (violations.length > 0) { allViolations.push(...violations); } }); return allViolations; } /** * Format and display violations */ function displayViolations(violations) { if (violations.length === 0) { console.log('āœ… No CSP violations found!\n'); return 0; } console.log(`āŒ Found ${violations.length} CSP violation(s):\n`); // Group by file const byFile = {}; violations.forEach(v => { if (!byFile[v.file]) byFile[v.file] = []; byFile[v.file].push(v); }); // Display by file Object.entries(byFile).forEach(([file, fileViolations]) => { console.log(`šŸ“„ ${file} (${fileViolations.length} violation(s)):`); fileViolations.forEach(v => { const severity = v.severity === 'CRITICAL' ? 'šŸ”“' : '🟔'; console.log(` ${severity} Line ${v.line}: ${v.description}`); console.log(` ${v.matched}`); }); console.log(''); }); // Summary by type console.log('šŸ“Š Violations by type:'); const byType = {}; violations.forEach(v => { byType[v.description] = (byType[v.description] || 0) + 1; }); Object.entries(byType).forEach(([type, count]) => { console.log(` - ${type}: ${count}`); }); console.log(`\nšŸ’” To fix violations, run: node scripts/fix-csp-violations.js\n`); return violations.length; } // Run scanner if called directly if (require.main === module) { const pattern = process.argv[2] || DEFAULT_PATTERNS; const violations = scanForViolations(pattern); const exitCode = displayViolations(violations); process.exit(exitCode > 0 ? 1 : 0); } // Export for use in other scripts module.exports = { scanForViolations, displayViolations, scanFile };