security-cron.js
1 #!/usr/bin/env node 2 /** 3 * Security Cron Job - Automated Security Maintenance 4 * 5 * This script is designed to be run by cron or Claude Code on a schedule. 6 * It runs comprehensive security scans and auto-fixes issues when safe to do so. 7 * 8 * Cron Schedule Recommendations: 9 * - Daily (recommended): 0 2 * * * (2 AM daily) 10 * - Weekly: 0 2 * * 0 (2 AM Sunday) 11 * - After deployments: On-demand via CI/CD 12 * 13 * Usage: 14 * node scripts/security-cron.js 15 * 16 * Environment Variables: 17 * SECURITY_AUTO_FIX=true - Enable automatic fixes (default: true) 18 * SECURITY_NOTIFY_EMAIL=email@example.com - Send report to email 19 */ 20 21 import { execSync } from 'child_process'; 22 import { existsSync, readFileSync, writeFileSync } from 'fs'; 23 import { join } from 'path'; 24 25 const AUTO_FIX = process.env.SECURITY_AUTO_FIX !== 'false'; // Default to true 26 const NOTIFY_EMAIL = process.env.SECURITY_NOTIFY_EMAIL || null; 27 const REPORTS_DIR = '.security-reports'; 28 const LAST_RUN_FILE = join(REPORTS_DIR, 'last-run.json'); 29 30 /** 31 * Log with timestamp 32 */ 33 function log(message, level = 'info') { 34 const timestamp = new Date().toISOString(); 35 const prefixes = { 36 info: 'đ', 37 success: 'â ', 38 error: 'â', 39 warn: 'â ī¸ ', 40 }; 41 // eslint-disable-next-line security/detect-object-injection -- Safe: level from our hardcoded strings 42 const prefix = prefixes[level] || 'âšī¸ '; 43 44 console.log(`[${timestamp}] ${prefix} ${message}`); 45 } 46 47 /** 48 * Check if we should run based on last run time 49 */ 50 function shouldRun() { 51 if (!existsSync(LAST_RUN_FILE)) { 52 return true; 53 } 54 55 try { 56 const lastRun = JSON.parse(readFileSync(LAST_RUN_FILE, 'utf8')); 57 const lastRunTime = new Date(lastRun.timestamp); 58 const hoursSinceLastRun = (Date.now() - lastRunTime.getTime()) / (1000 * 60 * 60); 59 60 // Don't run if last run was less than 12 hours ago 61 if (hoursSinceLastRun < 12) { 62 log(`Skipping: Last run was ${hoursSinceLastRun.toFixed(1)} hours ago`, 'info'); 63 return false; 64 } 65 66 return true; 67 } catch (error) { 68 log(`Error reading last run file: ${error.message}`, 'warn'); 69 return true; 70 } 71 } 72 73 /** 74 * Update last run timestamp 75 */ 76 function updateLastRun(results) { 77 const data = { 78 timestamp: new Date().toISOString(), 79 autofix: AUTO_FIX, 80 summary: results.summary || 'Security scan completed', 81 details: results.details || {}, 82 metrics: results.metrics || {}, 83 status: results.status, 84 }; 85 86 try { 87 writeFileSync(LAST_RUN_FILE, JSON.stringify(data, null, 2)); 88 log(`Last run data saved with summary: ${data.summary}`, 'info'); 89 } catch (error) { 90 log(`Failed to update last run file: ${error.message}`, 'error'); 91 } 92 } 93 94 /** 95 * Send notification email (if configured) 96 */ 97 function sendNotification(_results) { 98 if (!NOTIFY_EMAIL) { 99 return; 100 } 101 102 // TODO: Implement email notification 103 // Could use the existing Resend integration 104 log(`TODO: Send email notification to ${NOTIFY_EMAIL}`, 'info'); 105 } 106 107 /** 108 * Main cron routine 109 */ 110 function main() { 111 log('Security Cron Job Started', 'info'); 112 log(`Auto-fix enabled: ${AUTO_FIX}`, 'info'); 113 114 // Check if we should run 115 if (!shouldRun()) { 116 log('Exiting early - too soon since last run', 'info'); 117 process.exit(0); 118 } 119 120 const startTime = Date.now(); 121 122 try { 123 // Run security scan with auto-fix if enabled 124 const fixFlag = AUTO_FIX ? '--fix' : ''; 125 const command = `node scripts/security-scan.js ${fixFlag} --verbose`; 126 127 log(`Running: ${command}`, 'info'); 128 129 execSync(command, { 130 stdio: 'inherit', 131 cwd: process.cwd(), 132 }); 133 134 const duration = ((Date.now() - startTime) / 1000).toFixed(2); 135 136 const results = { 137 status: 'success', 138 summary: `Security scan completed in ${duration}s with auto-fix ${AUTO_FIX ? 'enabled' : 'disabled'}`, 139 details: { 140 autofix_enabled: AUTO_FIX, 141 duration_seconds: parseFloat(duration), 142 command, 143 timestamp: new Date().toISOString(), 144 description: 145 'Ran comprehensive security scans including npm audit, ESLint security, and optional Snyk/Semgrep checks', 146 }, 147 metrics: { 148 duration_seconds: parseFloat(duration), 149 autofix_enabled: AUTO_FIX ? 1 : 0, 150 success: 1, 151 }, 152 }; 153 154 updateLastRun(results); 155 sendNotification(results); 156 157 log('Security cron completed successfully', 'success'); 158 log(`Summary: ${results.summary}`, 'info'); 159 process.exit(0); 160 } catch (error) { 161 const duration = ((Date.now() - startTime) / 1000).toFixed(2); 162 163 const results = { 164 status: 'failed', 165 summary: `Security scan failed after ${duration}s: ${error.message}`, 166 details: { 167 autofix_enabled: AUTO_FIX, 168 error_message: error.message, 169 exit_code: error.status || 1, 170 duration_seconds: parseFloat(duration), 171 timestamp: new Date().toISOString(), 172 }, 173 metrics: { 174 duration_seconds: parseFloat(duration), 175 autofix_enabled: AUTO_FIX ? 1 : 0, 176 success: 0, 177 failed: 1, 178 }, 179 }; 180 181 updateLastRun(results); 182 sendNotification(results); 183 184 log(`Security cron failed: ${error.message}`, 'error'); 185 log('Critical security issues found - manual intervention required', 'error'); 186 187 // Exit with error to alert monitoring systems 188 process.exit(1); 189 } 190 } 191 192 // Run the cron job 193 main().catch(error => { 194 log(`Fatal error: ${error.message}`, 'error'); 195 process.exit(1); 196 });