/ scripts / security-cron.js
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  });