#!/usr/bin/env node /** * CSP Hash Collector * * This script scans all HTML files in a directory structure, * extracts inline scripts and styles, and calculates SHA-256 hashes * for use in Content Security Policy headers. * * Usage: * node collect-all-csp-hashes.js * * Example: * node collect-all-csp-hashes.js ./docker/showerloop/public */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); // Function to calculate CSP hash function calculateHash(content) { const hash = crypto.createHash('sha256').update(content, 'utf8').digest('base64'); return `'sha256-${hash}'`; } // Function to extract inline scripts from HTML function extractInlineScripts(html) { const scriptRegex = /]*)?>([\s\S]*?)<\/script>/gi; const scripts = []; let match; while ((match = scriptRegex.exec(html)) !== null) { const scriptContent = match[1].trim(); if (scriptContent && !scriptContent.includes('src=')) { scripts.push(scriptContent); } } return scripts; } // Function to extract inline styles from HTML function extractInlineStyles(html) { const styleRegex = /]*)?>([\s\S]*?)<\/style>/gi; const styles = []; let match; while ((match = styleRegex.exec(html)) !== null) { const styleContent = match[1].trim(); if (styleContent) { styles.push(styleContent); } } return styles; } // Function to find all HTML files in a directory recursively function findHtmlFiles(dir, fileList = []) { const files = fs.readdirSync(dir); files.forEach(file => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { findHtmlFiles(filePath, fileList); } else if (path.extname(file).toLowerCase() === '.html') { fileList.push(filePath); } }); return fileList; } // Main function function main() { if (process.argv.length < 3) { console.error('Error: Please provide a directory path.'); process.exit(1); } const directoryPath = process.argv[2]; try { console.log(`\nScanning directory: ${directoryPath}`); const htmlFiles = findHtmlFiles(directoryPath); console.log(`Found ${htmlFiles.length} HTML files.`); const scriptHashes = new Set(); const styleHashes = new Set(); let totalScripts = 0; let totalStyles = 0; htmlFiles.forEach(filePath => { try { const html = fs.readFileSync(filePath, 'utf8'); const scripts = extractInlineScripts(html); const styles = extractInlineStyles(html); totalScripts += scripts.length; totalStyles += styles.length; scripts.forEach(script => { const hash = calculateHash(script); scriptHashes.add(hash); }); styles.forEach(style => { const hash = calculateHash(style); styleHashes.add(hash); }); } catch (err) { console.error(`Error processing file ${filePath}: ${err.message}`); } }); console.log(`\nProcessed ${totalScripts} inline scripts and ${totalStyles} inline styles.`); console.log(`Found ${scriptHashes.size} unique script hashes and ${styleHashes.size} unique style hashes.`); console.log('\n=== Complete CSP Directives ==='); console.log('\nScript CSP directive:'); console.log(`script-src 'self' blob: ${Array.from(scriptHashes).join(' ')}`); console.log('\nStyle CSP directive:'); console.log(`style-src 'self' 'unsafe-inline' ${Array.from(styleHashes).join(' ')}`); console.log('\nComplete CSP header:'); console.log(`Content-Security-Policy: default-src 'self'; script-src 'self' blob: ${Array.from(scriptHashes).join(' ')}; style-src 'self' ${Array.from(styleHashes).join(' ')}; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; worker-src 'self' blob:"`); } catch (err) { console.error(`Error: ${err.message}`); process.exit(1); } } main();