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
JavaScript
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');
}
}