feat(infrastructure): add MongoDB sync and CSP compliance checking
DATABASE SYNC INFRASTRUCTURE:
- scripts/sync-instructions-to-db.js
- Syncs .claude/instruction-history.json to MongoDB governanceRules collection
- Handles inserts, updates, and deactivations
- Validates file and database counts match
- Used in governance audit (54 → 56 → 59 active rules)
- Required for production deployment of governance rules
CSP COMPLIANCE CHECKING:
- scripts/check-csp-violations.js
- Enforces Content Security Policy compliance (inst_008)
- Checks staged HTML files for:
- Inline scripts (<script> tags with code)
- Inline event handlers (onclick, onload, etc.)
- Inline styles (style attributes)
- Integrated with .git/hooks/pre-commit
- Blocks commits with CSP violations
REASON FOR CREATION:
- sync-instructions-to-db.js: Needed to deploy governance rules to production
- check-csp-violations.js: Pre-commit hook was calling missing script
USAGE:
- Sync to DB: node scripts/sync-instructions-to-db.js
- CSP check: Runs automatically on git commit (via pre-commit hook)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
af58ce01b6
commit
29011dfd00
2 changed files with 311 additions and 0 deletions
113
scripts/check-csp-violations.js
Executable file
113
scripts/check-csp-violations.js
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
#!/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
|
||||
198
scripts/sync-instructions-to-db.js
Executable file
198
scripts/sync-instructions-to-db.js
Executable file
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Sync Instructions to MongoDB
|
||||
*
|
||||
* Syncs .claude/instruction-history.json to MongoDB governanceRules collection
|
||||
* Handles: inserts, updates, deactivations
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mongoose = require('mongoose');
|
||||
require('dotenv').config();
|
||||
|
||||
const GovernanceRule = require('../src/models/GovernanceRule.model');
|
||||
|
||||
const INSTRUCTION_FILE = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
|
||||
async function syncToDatabase() {
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(' SYNC INSTRUCTIONS TO MONGODB');
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
console.log('📡 Connecting to MongoDB...');
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log(` ✓ Connected to ${MONGODB_URI}\n`);
|
||||
|
||||
// Read instruction history
|
||||
console.log('📖 Reading instruction-history.json...');
|
||||
const data = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
|
||||
console.log(` ✓ Version: ${data.version}`);
|
||||
console.log(` ✓ Total instructions: ${data.instructions.length}`);
|
||||
console.log(` ✓ Active instructions: ${data.instructions.filter(i => i.active !== false).length}\n`);
|
||||
|
||||
// Get current rules from database
|
||||
console.log('📊 Fetching current rules from database...');
|
||||
const existingRules = await GovernanceRule.find({});
|
||||
const existingRuleIds = new Set(existingRules.map(r => r.id));
|
||||
console.log(` ✓ Found ${existingRules.length} existing rules\n`);
|
||||
|
||||
// Sync stats
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
let deactivated = 0;
|
||||
let skipped = 0;
|
||||
const errors = [];
|
||||
|
||||
console.log('🔄 Syncing instructions...\n');
|
||||
|
||||
// Process each instruction
|
||||
for (const inst of data.instructions) {
|
||||
try {
|
||||
const ruleData = {
|
||||
id: inst.id,
|
||||
text: inst.text,
|
||||
quadrant: inst.quadrant,
|
||||
persistence: inst.persistence,
|
||||
temporalScope: inst.temporal_scope || 'PERMANENT',
|
||||
active: inst.active !== false,
|
||||
notes: inst.notes || '',
|
||||
source: inst.session_id ? 'user_instruction' : 'framework_default',
|
||||
createdBy: 'claude-code'
|
||||
};
|
||||
|
||||
// Handle additional fields if present
|
||||
if (inst.parameters) {
|
||||
// Store parameters as notes if not already in notes
|
||||
if (!ruleData.notes.includes('Parameters:')) {
|
||||
ruleData.notes += `\n\nParameters: ${JSON.stringify(inst.parameters)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (inst.deprecates) {
|
||||
if (!ruleData.notes.includes('Deprecates:')) {
|
||||
ruleData.notes += `\n\nDeprecates: ${inst.deprecates.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (inst.replaces) {
|
||||
if (!ruleData.notes.includes('Replaces:')) {
|
||||
ruleData.notes += `\n\nReplaces: ${inst.replaces.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (inst.part_of) {
|
||||
if (!ruleData.notes.includes('Part of:')) {
|
||||
ruleData.notes += `\n\nPart of: ${inst.part_of}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (inst.created_date) {
|
||||
if (!ruleData.notes.includes('Created:')) {
|
||||
ruleData.notes += `\n\nCreated: ${inst.created_date}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (inst.deprecation_reason) {
|
||||
if (!ruleData.notes.includes('Deprecation reason:')) {
|
||||
ruleData.notes += `\n\nDeprecation reason: ${inst.deprecation_reason}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up notes (remove leading/trailing whitespace)
|
||||
ruleData.notes = ruleData.notes.trim();
|
||||
|
||||
if (existingRuleIds.has(inst.id)) {
|
||||
// Update existing rule
|
||||
const result = await GovernanceRule.findOneAndUpdate(
|
||||
{ id: inst.id },
|
||||
ruleData,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (result) {
|
||||
updated++;
|
||||
console.log(` ↻ Updated ${inst.id}`);
|
||||
} else {
|
||||
errors.push({ id: inst.id, error: 'Update returned null' });
|
||||
console.log(` ✗ Failed to update ${inst.id}`);
|
||||
}
|
||||
} else {
|
||||
// Insert new rule
|
||||
const newRule = new GovernanceRule(ruleData);
|
||||
await newRule.save();
|
||||
inserted++;
|
||||
console.log(` + Inserted ${inst.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ id: inst.id, error: err.message });
|
||||
console.log(` ✗ Error processing ${inst.id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Deactivate rules that no longer exist in JSON
|
||||
const jsonRuleIds = new Set(data.instructions.map(i => i.id));
|
||||
for (const existingRule of existingRules) {
|
||||
if (!jsonRuleIds.has(existingRule.id) && existingRule.active) {
|
||||
existingRule.active = false;
|
||||
existingRule.notes += `\n\nDeactivated during sync on ${new Date().toISOString()} - no longer in instruction-history.json`;
|
||||
await existingRule.save();
|
||||
deactivated++;
|
||||
console.log(` ⊝ Deactivated ${existingRule.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (deactivated > 0) console.log('');
|
||||
|
||||
// Summary
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(' SYNC SUMMARY');
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
console.log(` Inserted: ${inserted}`);
|
||||
console.log(` Updated: ${updated}`);
|
||||
console.log(` Deactivated: ${deactivated}`);
|
||||
console.log(` Errors: ${errors.length}`);
|
||||
console.log('');
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(' Errors encountered:');
|
||||
errors.forEach(({ id, error }) => {
|
||||
console.log(` - ${id}: ${error}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Verify final counts
|
||||
const activeCount = await GovernanceRule.countDocuments({ active: true });
|
||||
const totalCount = await GovernanceRule.countDocuments({});
|
||||
|
||||
console.log(` Database: ${activeCount} active / ${totalCount} total`);
|
||||
console.log(` JSON file: ${data.stats.active_instructions} active / ${data.stats.total_instructions} total`);
|
||||
console.log('');
|
||||
|
||||
if (activeCount === data.stats.active_instructions) {
|
||||
console.log('✅ Sync successful - counts match!');
|
||||
} else {
|
||||
console.log('⚠️ WARNING: Active counts do not match');
|
||||
console.log(` Expected ${data.stats.active_instructions}, got ${activeCount}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Sync failed:', err.message);
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
console.log('📡 Disconnected from MongoDB\n');
|
||||
}
|
||||
}
|
||||
|
||||
syncToDatabase();
|
||||
Loading…
Add table
Reference in a new issue