Phase 2 Implementation: - Add RSS feed discovery links to footer (Subscribe section) - Create email templates (base-template.html, research-updates-content.html) - Add comprehensive newsletter sending implementation plan - Fix CSP check to exclude email-templates directory Email templates use inline styles for cross-client compatibility (Gmail, Outlook, Apple Mail) and are excluded from CSP checks. Next steps: Install dependencies (handlebars, @sendgrid/mail), implement EmailService, controller methods, and admin UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
146 lines
4.1 KiB
JavaScript
Executable file
146 lines
4.1 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* CSP Violations Checker
|
|
* Enforces Content Security Policy compliance (inst_008)
|
|
*
|
|
* Checks staged files for:
|
|
* - Inline scripts (<script> tags with code)
|
|
* - Inline event handlers (onclick, onload, etc.)
|
|
* - Inline styles in HTML
|
|
*
|
|
* Does NOT check:
|
|
* - Non-HTML files (JS, CSS, MD, etc.)
|
|
* - <script src="..."> external scripts
|
|
*/
|
|
|
|
const { execSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Scan for CSP violations in HTML files
|
|
* @param {Array<string>} files - Optional array of files to scan (defaults to staged files)
|
|
* @returns {Array} Array of violation objects
|
|
*/
|
|
function scanForViolations(files = null) {
|
|
let htmlFiles;
|
|
|
|
if (files) {
|
|
// Use provided files
|
|
htmlFiles = files.filter(f => f.endsWith('.html'));
|
|
} else {
|
|
// Get list of staged files
|
|
try {
|
|
const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACMR', { encoding: 'utf8' })
|
|
.split('\n')
|
|
.filter(f => f.trim() !== '');
|
|
htmlFiles = stagedFiles.filter(f => f.endsWith('.html'));
|
|
} catch (error) {
|
|
console.error('Error getting staged files:', error.message);
|
|
return []; // Return empty array if can't check
|
|
}
|
|
}
|
|
|
|
// Exclude email templates - they require inline styles for email client compatibility
|
|
htmlFiles = htmlFiles.filter(f => !f.startsWith('email-templates/'));
|
|
|
|
if (htmlFiles.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const violations = [];
|
|
|
|
// Check each HTML file
|
|
htmlFiles.forEach(file => {
|
|
const filePath = path.join(process.cwd(), file);
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return; // File deleted, skip
|
|
}
|
|
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const lines = content.split('\n');
|
|
|
|
lines.forEach((line, index) => {
|
|
const lineNum = index + 1;
|
|
|
|
// Check for inline scripts (but not <script src="...">)
|
|
if (/<script(?!.*src=)[^>]*>[\s\S]*?<\/script>/i.test(line)) {
|
|
if (line.includes('<script>') || (line.includes('<script ') && !line.includes('src='))) {
|
|
violations.push({
|
|
file,
|
|
line: lineNum,
|
|
type: 'inline-script',
|
|
content: line.trim().substring(0, 80)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for inline event handlers
|
|
const inlineHandlers = ['onclick', 'onload', 'onmouseover', 'onsubmit', 'onerror', 'onchange'];
|
|
inlineHandlers.forEach(handler => {
|
|
if (new RegExp(`\\s${handler}=`, 'i').test(line)) {
|
|
violations.push({
|
|
file,
|
|
line: lineNum,
|
|
type: 'inline-handler',
|
|
handler,
|
|
content: line.trim().substring(0, 80)
|
|
});
|
|
}
|
|
});
|
|
|
|
// Check for inline styles (style attribute)
|
|
if (/\sstyle\s*=\s*["'][^"']*["']/i.test(line)) {
|
|
// Allow Tailwind utility classes pattern (common false positive)
|
|
if (!line.includes('class=') || line.match(/style\s*=\s*["'][^"']{20,}/)) {
|
|
violations.push({
|
|
file,
|
|
line: lineNum,
|
|
type: 'inline-style',
|
|
content: line.trim().substring(0, 80)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
return violations;
|
|
}
|
|
|
|
/**
|
|
* Display violations to console
|
|
* @param {Array} violations - Array of violation objects
|
|
*/
|
|
function displayViolations(violations) {
|
|
if (violations.length === 0) {
|
|
return;
|
|
}
|
|
|
|
console.error('\nCSP Violations Found:\n');
|
|
violations.forEach(v => {
|
|
console.error(` ${v.file}:${v.line}`);
|
|
console.error(` Type: ${v.type}${v.handler ? ' (' + v.handler + ')' : ''}`);
|
|
console.error(` Content: ${v.content}`);
|
|
console.error('');
|
|
});
|
|
|
|
console.error(`Total violations: ${violations.length}\n`);
|
|
console.error('Fix these violations or use --no-verify to bypass (not recommended)\n');
|
|
}
|
|
|
|
// Export functions for use as a module
|
|
module.exports = { scanForViolations, displayViolations };
|
|
|
|
// Run as CLI if called directly
|
|
if (require.main === module) {
|
|
const violations = scanForViolations();
|
|
|
|
if (violations.length === 0) {
|
|
process.exit(0); // No violations, allow commit
|
|
}
|
|
|
|
displayViolations(violations);
|
|
process.exit(1); // Block commit
|
|
}
|