security-scan.js
1 #!/usr/bin/env node 2 /** 3 * Comprehensive Security Scan Script 4 * 5 * Runs all security checks and generates a report. 6 * Designed to be run by cron or CI/CD. 7 * Can auto-fix certain issues with --fix flag. 8 * 9 * Usage: 10 * node scripts/security-scan.js [--fix] [--verbose] 11 */ 12 13 // Safe: all commands are hardcoded npm scripts; file paths are from known report directories 14 /* eslint-disable security/detect-non-literal-fs-filename */ 15 import { execSync } from 'child_process'; 16 import { existsSync, mkdirSync, writeFileSync } from 'fs'; 17 import { join } from 'path'; 18 19 const REPORTS_DIR = '.security-reports'; 20 const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-'); 21 22 // Parse CLI args 23 const args = process.argv.slice(2); 24 const autoFix = args.includes('--fix'); 25 const verbose = args.includes('--verbose'); 26 27 // Ensure reports directory exists 28 if (!existsSync(REPORTS_DIR)) { 29 mkdirSync(REPORTS_DIR, { recursive: true }); 30 } 31 32 const results = { 33 timestamp: new Date().toISOString(), 34 autofix: autoFix, 35 checks: [], 36 summary: { 37 total: 0, 38 passed: 0, 39 failed: 0, 40 fixed: 0, 41 }, 42 }; 43 44 /** 45 * Run a command and capture output 46 */ 47 function runCheck(name, command, options = {}) { 48 const { critical = false, canFix = false } = options; 49 50 console.log(`\nš Running ${name}...`); 51 52 const check = { 53 name, 54 command, 55 critical, 56 canFix, 57 status: 'pending', 58 output: '', 59 error: '', 60 exitCode: 0, 61 }; 62 63 try { 64 // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process 65 const output = execSync(command, { 66 encoding: 'utf8', 67 stdio: verbose ? 'inherit' : 'pipe', 68 cwd: process.cwd(), 69 }); 70 71 check.output = output; 72 check.status = 'passed'; 73 check.exitCode = 0; 74 75 console.log(`ā ${name} passed`); 76 results.summary.passed++; 77 } catch (error) { 78 check.error = error.stderr || error.stdout || error.message; 79 check.exitCode = error.status || 1; 80 check.status = 'failed'; 81 82 console.error(`ā ${name} failed (exit code: ${check.exitCode})`); 83 84 if (verbose) { 85 console.error(check.error); 86 } 87 88 // Try to fix if auto-fix is enabled and this check supports it 89 if (autoFix && canFix) { 90 const fixCommand = getFixCommand(name); 91 if (fixCommand) { 92 console.log(`š§ Attempting to fix ${name}...`); 93 try { 94 // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process 95 execSync(fixCommand, { stdio: 'inherit' }); 96 check.status = 'fixed'; 97 check.fixCommand = fixCommand; 98 results.summary.fixed++; 99 console.log(`ā ${name} fixed`); 100 } catch { 101 console.error(`ā ļø Could not auto-fix ${name}`); 102 results.summary.failed++; 103 } 104 } 105 } else { 106 results.summary.failed++; 107 } 108 } 109 110 results.checks.push(check); 111 results.summary.total++; 112 113 return check.status === 'passed' || check.status === 'fixed'; 114 } 115 116 /** 117 * Get fix command for a specific check 118 */ 119 function getFixCommand(checkName) { 120 const fixes = { 121 'ESLint Security': 'npm run lint:fix', 122 'npm Audit': 'npm audit fix', 123 Snyk: 'snyk fix', 124 }; 125 126 // Safe: checkName is from our hardcoded check names, not user input 127 // eslint-disable-next-line security/detect-object-injection 128 return fixes[checkName] || null; 129 } 130 131 /** 132 * Main security scan routine 133 */ 134 function main() { 135 console.log('š”ļø Starting Security Scan'); 136 console.log(`Auto-fix: ${autoFix ? 'enabled' : 'disabled'}`); 137 console.log(`Timestamp: ${results.timestamp}`); 138 139 // 1. ESLint Security Checks (fast, can fix) 140 runCheck('ESLint Security', 'npm run security:lint', { critical: true, canFix: true }); 141 142 // 2. npm Audit (fast, can fix) 143 runCheck('npm Audit', 'npm run security:audit', { critical: true, canFix: true }); 144 145 // 3. Snyk (medium speed, can fix) 146 // Only run if snyk is installed 147 try { 148 execSync('which snyk', { stdio: 'ignore' }); 149 runCheck('Snyk', 'npm run security:snyk', { critical: false, canFix: true }); 150 } catch { 151 console.log('āļø Skipping Snyk (not installed)'); 152 console.log(' Install with: npm install -g snyk && snyk auth'); 153 } 154 155 // 4. Semgrep (slower, manual fix) 156 // Only run if semgrep is installed 157 try { 158 execSync('which semgrep', { stdio: 'ignore' }); 159 runCheck('Semgrep', 'npm run security:semgrep', { critical: false, canFix: false }); 160 } catch { 161 console.log('āļø Skipping Semgrep (not installed)'); 162 console.log(' Install with: pip install semgrep OR brew install semgrep'); 163 } 164 165 // 5. Check for hardcoded secrets (fast, manual fix) 166 console.log('\nš Scanning for hardcoded secrets...'); 167 try { 168 // Simple keyword-based secret detection 169 // Look for common secret-related keywords 170 const keywords = ['API_KEY', 'SECRET', 'PASSWORD', 'PRIVATE_KEY', 'TOKEN']; 171 172 let secretsFound = ''; 173 for (const keyword of keywords) { 174 try { 175 // Simple grep for the keyword followed by = and a quoted string 176 const result = execSync( 177 `grep -r "${keyword}.*=" src/ scripts/ --exclude-dir=node_modules --exclude-dir=.git --exclude="*.test.js" 2>/dev/null || true`, 178 { 179 encoding: 'utf8', 180 } 181 ); 182 183 // Filter out .env references and process.env usage (those are safe) 184 const lines = result 185 .split('\n') 186 .filter( 187 line => 188 line && 189 !line.includes('process.env') && 190 !line.includes('.env') && 191 !line.includes('// ') 192 ); 193 194 if (lines.length > 0) { 195 secretsFound += `${lines.join('\n')}\n`; 196 } 197 } catch { 198 // Ignore grep errors 199 } 200 } 201 202 if (secretsFound.trim()) { 203 console.error('ā ļø Potential secrets found in code:'); 204 console.error(secretsFound); 205 results.checks.push({ 206 name: 'Secret Detection', 207 status: 'failed', 208 output: secretsFound, 209 critical: true, 210 canFix: false, 211 }); 212 results.summary.failed++; 213 } else { 214 console.log('ā No hardcoded secrets detected'); 215 results.checks.push({ 216 name: 'Secret Detection', 217 status: 'passed', 218 critical: true, 219 }); 220 results.summary.passed++; 221 } 222 results.summary.total++; 223 } catch (error) { 224 console.error('ā ļø Could not scan for secrets:', error.message); 225 } 226 227 // Generate report 228 const reportPath = join(REPORTS_DIR, `security-scan-${TIMESTAMP}.json`); 229 writeFileSync(reportPath, JSON.stringify(results, null, 2)); 230 231 // Generate summary 232 console.log('\nš Security Scan Summary'); 233 console.log('ā'.repeat(50)); 234 console.log(`Total checks: ${results.summary.total}`); 235 console.log(`ā Passed: ${results.summary.passed}`); 236 console.log(`ā Failed: ${results.summary.failed}`); 237 if (autoFix) { 238 console.log(`š§ Fixed: ${results.summary.fixed}`); 239 } 240 console.log(`\nš Full report: ${reportPath}`); 241 242 // Exit with error if critical checks failed 243 const criticalFailures = results.checks.filter(c => c.critical && c.status === 'failed'); 244 245 if (criticalFailures.length > 0) { 246 console.error(`\nā ${criticalFailures.length} critical security issues found!`); 247 criticalFailures.forEach(check => { 248 console.error(` - ${check.name}`); 249 }); 250 process.exit(1); 251 } 252 253 console.log('\nā Security scan complete'); 254 process.exit(0); 255 } 256 257 // Run the scan 258 main().catch(error => { 259 console.error('Fatal error during security scan:', error); 260 process.exit(1); 261 });