- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
113 lines
3.1 KiB
JavaScript
Executable file
113 lines
3.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');
|
|
|
|
// Get list of staged files
|
|
let stagedFiles;
|
|
try {
|
|
stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACMR', { encoding: 'utf8' })
|
|
.split('\n')
|
|
.filter(f => f.trim() !== '');
|
|
} catch (error) {
|
|
console.error('Error getting staged files:', error.message);
|
|
process.exit(0); // Allow commit if can't check
|
|
}
|
|
|
|
// Filter to HTML files only
|
|
const htmlFiles = stagedFiles.filter(f => f.endsWith('.html'));
|
|
|
|
if (htmlFiles.length === 0) {
|
|
// No HTML files, nothing to check
|
|
process.exit(0);
|
|
}
|
|
|
|
let 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)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
if (violations.length === 0) {
|
|
process.exit(0); // No violations, allow commit
|
|
}
|
|
|
|
// Report violations
|
|
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');
|
|
|
|
process.exit(1); // Block commit
|