UNPKG

vibe-code-build

Version:

Real-time code monitoring with teaching explanations, CLAUDE.md compliance checking, and interactive chat

782 lines (695 loc) 24.7 kB
import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import ora from 'ora'; import crypto from 'crypto'; export class SecurityChecker { constructor(projectPath = process.cwd(), options = {}) { this.projectPath = projectPath; this.options = options; this.results = { godMode: null, secrets: null, vulnerabilities: null, permissions: null, aiRisks: null }; } async checkAll() { const spinner = this.options.silent ? null : ora('Running security checks...').start(); try { if (spinner) spinner.text = 'Checking for AI god mode patterns...'; this.results.godMode = await this.checkGodMode(); if (spinner) spinner.text = 'Scanning for exposed secrets...'; this.results.secrets = await this.checkSecrets(); if (spinner) spinner.text = 'Checking for security vulnerabilities...'; this.results.vulnerabilities = await this.checkVulnerabilities(); if (spinner) spinner.text = 'Checking file permissions...'; this.results.permissions = await this.checkPermissions(); if (spinner) spinner.text = 'Analyzing AI-specific risks...'; this.results.aiRisks = await this.checkAIRisks(); if (spinner) spinner.succeed('Security checks completed'); return this.results; } catch (error) { if (spinner) spinner.fail('Security checks failed'); throw error; } } async checkGodMode() { const godModePatterns = [ { pattern: /sudo\s+rm\s+-rf\s+\//g, severity: 'critical', description: 'Dangerous system deletion command' }, { pattern: /process\.exit\(\s*\)/g, severity: 'high', description: 'Forceful process termination' }, { pattern: /require\(['"]child_process['"]\)\.exec\([^)]*rm/g, severity: 'critical', description: 'Executing system deletion via child process' }, { pattern: /eval\s*\(/g, severity: 'critical', description: 'Dynamic code execution (eval)', contextCheck: true }, { pattern: /new\s+Function\s*\(/g, severity: 'critical', description: 'Dynamic function creation', contextCheck: true }, { pattern: /\.writeFileSync\s*\(\s*['"]\/etc\//g, severity: 'critical', description: 'Writing to system configuration' }, { pattern: /process\.env\.\w+\s*=\s*["']/g, severity: 'high', description: 'Modifying process environment' }, { pattern: /require\(['"]fs['"]\)\.unlinkSync\s*\(\s*['"]\/(?!tmp)/g, severity: 'critical', description: 'Deleting system files' }, { pattern: /exec\s*\(\s*['"]chmod\s+777/g, severity: 'high', description: 'Setting dangerous file permissions' }, { pattern: /while\s*\(\s*true\s*\)/g, severity: 'medium', description: 'Infinite loop detected' } ]; const findings = []; const files = await this.findCodeFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); for (const check of godModePatterns) { const matches = content.matchAll(check.pattern); for (const match of matches) { // Skip false positives for contextCheck patterns if (check.contextCheck && this.isSecurityDetectionCode(content, match.index)) { continue; } const lineNumber = content.substring(0, match.index).split('\n').length; const finding = { file: path.relative(this.projectPath, file), line: lineNumber, severity: check.severity, description: check.description, code: match[0].substring(0, 50) }; // Determine environment and adjust severity finding.environment = this.determineEnvironment(file, content, finding); finding.originalSeverity = finding.severity; // Adjust severity for non-production code if (finding.environment === 'test') { finding.severity = this.adjustSeverityForTest(finding.severity); finding.isRealThreat = false; } else if (finding.environment === 'local') { finding.severity = this.adjustSeverityForLocal(finding.severity); finding.isRealThreat = false; } else { finding.isRealThreat = true; } findings.push(finding); } } } catch {} } const prodFindings = findings.filter(f => f.isRealThreat); const testFindings = findings.filter(f => !f.isRealThreat); const criticalCount = prodFindings.filter(f => f.originalSeverity === 'critical').length; const highCount = prodFindings.filter(f => f.originalSeverity === 'high').length; return { status: criticalCount > 0 ? 'failed' : highCount > 2 ? 'warning' : 'passed', message: prodFindings.length > 0 ? `Found ${prodFindings.length} production threats (${criticalCount} critical)` : 'No production threats detected', findings: findings.slice(0, 10), productionThreats: prodFindings.length, testThreats: testFindings.length, summary: { critical: criticalCount, high: highCount, medium: findings.filter(f => f.severity === 'medium').length, total: findings.length } }; } async checkSecrets() { const secretPatterns = [ { pattern: /(['"]?)(?:api[_-]?key|apikey)(['"]?)\s*[:=]\s*(['"])([^'"]+)\3/gi, type: 'API Key', severity: 'high' }, { pattern: /(['"]?)(?:secret[_-]?key|secret)(['"]?)\s*[:=]\s*(['"])([^'"]+)\3/gi, type: 'Secret Key', severity: 'high' }, { pattern: /(['"]?)password(['"]?)\s*[:=]\s*(['"])([^'"]+)\3/gi, type: 'Password', severity: 'critical' }, { pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/g, type: 'Bearer Token', severity: 'high' }, { pattern: /sk-[a-zA-Z0-9]{48}/g, type: 'OpenAI API Key', severity: 'critical' }, { pattern: /AIza[0-9A-Za-z\-_]{35}/g, type: 'Google API Key', severity: 'high' }, { pattern: /[0-9a-f]{40}/g, type: 'SHA1 Hash (possible token)', severity: 'medium' }, { pattern: /mongodb:\/\/[^:]+:[^@]+@/g, type: 'MongoDB Connection String', severity: 'critical' }, { pattern: /postgres:\/\/[^:]+:[^@]+@/g, type: 'PostgreSQL Connection String', severity: 'critical' } ]; const findings = []; const files = await this.findCodeFiles(); for (const file of files) { if (file.includes('node_modules') || file.includes('.git')) continue; try { const content = await fs.readFile(file, 'utf8'); for (const check of secretPatterns) { const matches = content.matchAll(check.pattern); for (const match of matches) { const value = match[0]; if (this.isLikelySecret(value, check.type)) { const lineNumber = content.substring(0, match.index).split('\n').length; const finding = { file: path.relative(this.projectPath, file), line: lineNumber, type: check.type, severity: check.severity, value: this.maskSecret(value) }; // Determine environment and adjust severity finding.environment = this.determineEnvironment(file, content, { code: value }); finding.originalSeverity = finding.severity; if (finding.environment === 'test') { finding.severity = this.adjustSeverityForTest(finding.severity); finding.isRealThreat = false; } else if (finding.environment === 'local') { finding.severity = this.adjustSeverityForLocal(finding.severity); finding.isRealThreat = false; } else { finding.isRealThreat = true; } findings.push(finding); } } } } catch {} } const prodFindings = findings.filter(f => f.isRealThreat); const testFindings = findings.filter(f => !f.isRealThreat); const criticalCount = prodFindings.filter(f => f.originalSeverity === 'critical').length; return { status: criticalCount > 0 ? 'failed' : prodFindings.length > 3 ? 'warning' : 'passed', message: prodFindings.length > 0 ? `Found ${prodFindings.length} production secrets (${criticalCount} critical)` : 'No production secrets detected', findings: findings.slice(0, 10), productionThreats: prodFindings.length, testThreats: testFindings.length, summary: { critical: criticalCount, high: findings.filter(f => f.severity === 'high').length, medium: findings.filter(f => f.severity === 'medium').length, total: findings.length } }; } async checkVulnerabilities() { const vulnerabilityPatterns = [ { pattern: /innerHTML\s*=\s*[^'"`]/g, type: 'XSS - Direct innerHTML', severity: 'high' }, { pattern: /dangerouslySetInnerHTML/g, type: 'XSS - React dangerous HTML', severity: 'medium' }, { pattern: /createObjectURL\s*\(/g, type: 'Object URL Creation', severity: 'low' }, { pattern: /window\.location\s*=\s*[^'"`]/g, type: 'Open Redirect', severity: 'high' }, { pattern: /SELECT\s+\*\s+FROM\s+\w+\s+WHERE\s+\w+\s*=\s*['"]?\s*\+/gi, type: 'SQL Injection', severity: 'critical' }, { pattern: /\$\{[^}]*\}.*SELECT.*FROM/gi, type: 'SQL Injection (template literal)', severity: 'critical' }, { pattern: /res\.send\([^)]*req\.(query|params|body)/g, type: 'Reflected Input', severity: 'medium' }, { pattern: /fs\.\w+Sync\s*\(\s*req\.(query|params|body)/g, type: 'Path Traversal', severity: 'critical' }, { pattern: /new\s+RegExp\s*\(\s*req\.(query|params|body)/g, type: 'ReDoS - User Controlled Regex', severity: 'high' }, { pattern: /JSON\.parse\s*\(\s*req\.(query|params|body)/g, type: 'JSON Injection', severity: 'medium' } ]; const findings = []; const files = await this.findCodeFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); for (const check of vulnerabilityPatterns) { const matches = content.matchAll(check.pattern); for (const match of matches) { const lineNumber = content.substring(0, match.index).split('\n').length; const finding = { file: path.relative(this.projectPath, file), line: lineNumber, type: check.type, severity: check.severity, code: match[0].substring(0, 60) }; // Determine environment and adjust severity finding.environment = this.determineEnvironment(file, content, finding); finding.originalSeverity = finding.severity; if (finding.environment === 'test') { finding.severity = this.adjustSeverityForTest(finding.severity); finding.isRealThreat = false; } else if (finding.environment === 'local') { finding.severity = this.adjustSeverityForLocal(finding.severity); finding.isRealThreat = false; } else { finding.isRealThreat = true; } findings.push(finding); } } } catch {} } const prodFindings = findings.filter(f => f.isRealThreat); const testFindings = findings.filter(f => !f.isRealThreat); const criticalCount = prodFindings.filter(f => f.originalSeverity === 'critical').length; const highCount = prodFindings.filter(f => f.originalSeverity === 'high').length; return { status: criticalCount > 0 ? 'failed' : highCount > 5 ? 'warning' : 'passed', message: prodFindings.length > 0 ? `Found ${prodFindings.length} production vulnerabilities (${criticalCount} critical)` : 'No production vulnerabilities detected', findings: findings.slice(0, 10), productionThreats: prodFindings.length, testThreats: testFindings.length, summary: { critical: criticalCount, high: highCount, medium: findings.filter(f => f.severity === 'medium').length, low: findings.filter(f => f.severity === 'low').length, total: findings.length } }; } async checkPermissions() { const suspiciousPermissions = []; try { const gitignorePath = path.join(this.projectPath, '.gitignore'); let gitignorePatterns = []; try { const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); gitignorePatterns = gitignoreContent.split('\n') .filter(line => line.trim() && !line.startsWith('#')) .map(line => line.trim()); } catch {} const sensitiveFiles = [ '.env', '.env.local', '.env.production', 'config.json', 'credentials.json', 'secrets.json', '.npmrc', 'private.key', 'cert.pem' ]; for (const file of sensitiveFiles) { const filePath = path.join(this.projectPath, file); try { const stats = await fs.stat(filePath); const mode = (stats.mode & parseInt('777', 8)).toString(8); const isIgnored = gitignorePatterns.some(pattern => file === pattern || file.includes(pattern) ); if (mode !== '600' && mode !== '400') { suspiciousPermissions.push({ file, permissions: mode, issue: 'Sensitive file has loose permissions', severity: isIgnored ? 'medium' : 'high', recommendation: `chmod 600 ${file}` }); } if (!isIgnored) { suspiciousPermissions.push({ file, permissions: mode, issue: 'Sensitive file not in .gitignore', severity: 'high', recommendation: `Add ${file} to .gitignore` }); } } catch {} } return { status: suspiciousPermissions.filter(p => p.severity === 'high').length > 0 ? 'warning' : 'passed', message: suspiciousPermissions.length > 0 ? `Found ${suspiciousPermissions.length} permission issues` : 'File permissions are properly configured', findings: suspiciousPermissions, total: suspiciousPermissions.length }; } catch (error) { return { status: 'error', message: 'Failed to check permissions', error: error.message }; } } async checkAIRisks() { const aiRiskPatterns = [ { pattern: /prompt\s*=.*\$\{.*user.*\}/gi, type: 'Prompt Injection Risk', severity: 'high', description: 'User input directly in AI prompt' }, { pattern: /system\s*:\s*.*\$\{.*\}/gi, type: 'System Prompt Manipulation', severity: 'critical', description: 'Dynamic system prompt construction' }, { pattern: /temperature\s*:\s*[0-9.]+/g, type: 'AI Temperature Setting', severity: 'info', description: 'AI randomness control' }, { pattern: /max_tokens\s*:\s*\d+/g, type: 'Token Limit Configuration', severity: 'info', description: 'Output length control' }, { pattern: /(?:openai|anthropic|cohere)\.(?:api_key|apiKey)/gi, type: 'AI API Key Reference', severity: 'high', description: 'AI service API key usage' }, { pattern: /generateCode|codeGeneration|aiGenerate/gi, type: 'AI Code Generation', severity: 'medium', description: 'Automated code generation detected' }, { pattern: /without\s+human\s+review|auto[_-]?merge|automatic[_-]?deploy/gi, type: 'Unsupervised AI Operation', severity: 'critical', description: 'AI operating without human oversight' } ]; const findings = []; const files = await this.findCodeFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); for (const check of aiRiskPatterns) { const matches = content.matchAll(check.pattern); for (const match of matches) { const lineNumber = content.substring(0, match.index).split('\n').length; findings.push({ file: path.relative(this.projectPath, file), line: lineNumber, type: check.type, severity: check.severity, description: check.description, code: match[0] }); } } } catch {} } const criticalCount = findings.filter(f => f.severity === 'critical').length; const highCount = findings.filter(f => f.severity === 'high').length; return { status: criticalCount > 0 ? 'failed' : highCount > 3 ? 'warning' : 'passed', message: findings.length > 0 ? `Found ${findings.length} AI-specific risks` : 'No AI-specific risks detected', findings: findings.slice(0, 10), summary: { critical: criticalCount, high: highCount, medium: findings.filter(f => f.severity === 'medium').length, info: findings.filter(f => f.severity === 'info').length, total: findings.length } }; } isLikelySecret(value, type) { if (value.length < 10) return false; if (value.includes('example') || value.includes('placeholder')) return false; if (value === 'xxxxxxxx' || value === '********') return false; const entropy = this.calculateEntropy(value); return entropy > 3.5; } calculateEntropy(str) { const freq = {}; for (const char of str) { freq[char] = (freq[char] || 0) + 1; } let entropy = 0; const len = str.length; for (const count of Object.values(freq)) { const p = count / len; entropy -= p * Math.log2(p); } return entropy; } maskSecret(value) { if (value.length <= 10) return '***'; return value.substring(0, 4) + '...' + value.substring(value.length - 4); } isTestOrLocalFile(filePath) { const testIndicators = [ '/test/', '/tests/', '/spec/', '/__tests__/', '/examples/', '/demo/', '/sample/', '.test.', '.spec.', '.test-', 'test-', 'demo-', 'example-', 'sample-', 'mock-', '/fixtures/', '/mocks/' ]; const normalizedPath = filePath.toLowerCase(); return testIndicators.some(indicator => normalizedPath.includes(indicator)); } isLocalOnlyPattern(content, matchText) { const localIndicators = [ 'localhost', '127.0.0.1', '0.0.0.0', 'test-api-key', 'test-secret', 'example-key', 'demo-password', 'your-api-key-here', 'your-secret-here', 'replace-this', 'todo: replace', 'fixme:', 'xxx', 'dummy', 'fake', 'mock' ]; const lowerContent = content.toLowerCase(); const lowerMatch = matchText.toLowerCase(); // Check if the matched text or surrounding context contains local indicators return localIndicators.some(indicator => lowerMatch.includes(indicator) || lowerContent.substring(Math.max(0, lowerContent.indexOf(lowerMatch) - 50), lowerContent.indexOf(lowerMatch) + lowerMatch.length + 50).includes(indicator) ); } determineEnvironment(filePath, content, finding) { // Check if it's a test file if (this.isTestOrLocalFile(filePath)) { return 'test'; } // Check if it contains local-only patterns if (this.isLocalOnlyPattern(content, finding.code || '')) { return 'local'; } // Check for development environment indicators if (filePath.includes('.env.local') || filePath.includes('.env.development') || filePath.includes('.env.example')) { return 'local'; } // Default to production (most conservative) return 'production'; } adjustSeverityForTest(severity) { // Downgrade severity for test files const adjustmentMap = { 'critical': 'medium', 'high': 'low', 'medium': 'low', 'low': 'info' }; return adjustmentMap[severity] || 'info'; } adjustSeverityForLocal(severity) { // Downgrade severity for local-only patterns const adjustmentMap = { 'critical': 'high', 'high': 'medium', 'medium': 'low', 'low': 'info' }; return adjustmentMap[severity] || 'info'; } isSecurityDetectionCode(content, matchIndex) { // Get the line containing the match and some context const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex); const afterMatch = content.substring(matchIndex, Math.min(content.length, matchIndex + 200)); const context = beforeMatch + afterMatch; // Look for patterns that indicate this is security detection code, not actual usage const detectionPatterns = [ /\.includes\s*\(\s*['"`].*eval/i, /\.includes\s*\(\s*['"`].*new\s+Function/i, /code\.includes/i, /content\.includes/i, /if\s*\(\s*.*\.includes.*eval/i, /if\s*\(\s*.*\.includes.*new\s+Function/i, /security.*check/i, /vulnerability.*detect/i, /pattern.*match/i, /insights\.security\.push/i, /analysis\.security/i, /checkVulnerabilities/i, /securityChecker/i ]; return detectionPatterns.some(pattern => pattern.test(context)); } async findCodeFiles() { const files = []; const extensions = ['.js', '.jsx', '.ts', '.tsx', '.json', '.env', '.yml', '.yaml']; async function scan(dir) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'build' && entry.name !== 'coverage') { await scan(fullPath); } else if (entry.isFile() && (extensions.some(ext => entry.name.endsWith(ext)) || entry.name.startsWith('.'))) { files.push(fullPath); } } } catch {} } await scan(this.projectPath); return files; } formatResults() { const sections = []; for (const [check, result] of Object.entries(this.results)) { if (!result) continue; const icon = result.status === 'passed' ? '✅' : result.status === 'failed' ? '❌' : result.status === 'warning' ? '⚠️' : '❓'; const color = result.status === 'passed' ? chalk.green : result.status === 'failed' ? chalk.red : result.status === 'warning' ? chalk.yellow : chalk.gray; sections.push(color(`${icon} ${check.replace(/([A-Z])/g, ' $1').toUpperCase()}: ${result.message}`)); if (result.summary) { if (result.summary.critical > 0) { sections.push(chalk.red(` 🚨 Critical: ${result.summary.critical}`)); } if (result.summary.high > 0) { sections.push(chalk.red(` ⚠️ High: ${result.summary.high}`)); } } } return sections.join('\n'); } }