UNPKG

embedia

Version:

Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys

280 lines (238 loc) • 8.5 kB
const fs = require('fs-extra'); const path = require('path'); const { glob } = require('glob'); const chalk = require('chalk'); class SecurityScanner { async scanForVulnerabilities(projectPath) { const vulnerabilities = []; // Check for exposed API keys const exposed = await this.checkExposedSecrets(projectPath); if (exposed.length > 0) { vulnerabilities.push({ severity: 'critical', type: 'exposed_secrets', files: exposed, message: 'Found exposed API keys or secrets', resolution: 'Move secrets to environment variables' }); } // Check for insecure API endpoints const insecureEndpoints = await this.checkAPISecurity(projectPath); vulnerabilities.push(...insecureEndpoints); // Check for missing security headers const headerIssues = await this.checkSecurityHeaders(projectPath); vulnerabilities.push(...headerIssues); // Check for unsafe dependencies const depIssues = await this.checkDependencies(projectPath); vulnerabilities.push(...depIssues); return vulnerabilities; } async checkExposedSecrets(projectPath) { const secretPatterns = [ /OPENAI_API_KEY\s*=\s*["']?sk-[a-zA-Z0-9]+/, /GEMINI_API_KEY\s*=\s*["']?[a-zA-Z0-9_-]+/, /ANTHROPIC_API_KEY\s*=\s*["']?[a-zA-Z0-9_-]+/, /api[_-]?key\s*[:=]\s*["']?[a-zA-Z0-9_-]{20,}/i, /secret[_-]?key\s*[:=]\s*["']?[a-zA-Z0-9_-]{20,}/i, /password\s*[:=]\s*["'][^"']+["']/i ]; const exposed = []; const files = await this.getSourceFiles(projectPath); for (const file of files) { try { const content = await fs.readFile(file, 'utf-8'); // Skip .env files if (file.includes('.env')) continue; for (const pattern of secretPatterns) { if (pattern.test(content)) { exposed.push(path.relative(projectPath, file)); break; } } } catch (error) { // Ignore read errors } } return exposed; } async checkAPISecurity(projectPath) { const issues = []; const apiFiles = await glob('**/api/**/*.{js,ts}', { cwd: projectPath, ignore: ['node_modules/**', '.next/**'] }); for (const file of apiFiles) { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); // Check for missing method validation if (!content.includes('method') && !content.includes('POST') && !content.includes('GET')) { issues.push({ severity: 'warning', type: 'missing_method_validation', file: file, message: 'API endpoint may not validate HTTP methods', resolution: 'Add method validation (e.g., if (req.method !== "POST"))' }); } // Check for missing rate limiting if (!content.includes('rate') && !content.includes('limit')) { issues.push({ severity: 'info', type: 'missing_rate_limiting', file: file, message: 'API endpoint has no rate limiting', resolution: 'Consider adding rate limiting middleware' }); } // Check for missing error handling if (!content.includes('try') || !content.includes('catch')) { issues.push({ severity: 'warning', type: 'missing_error_handling', file: file, message: 'API endpoint may lack proper error handling', resolution: 'Wrap in try-catch block' }); } } return issues; } async checkSecurityHeaders(projectPath) { const issues = []; // Check for Next.js security headers const configFiles = ['next.config.js', 'next.config.mjs']; let hasSecurityHeaders = false; for (const configFile of configFiles) { const configPath = path.join(projectPath, configFile); if (await fs.pathExists(configPath)) { const content = await fs.readFile(configPath, 'utf-8'); if (content.includes('headers') && (content.includes('X-Frame-Options') || content.includes('Content-Security-Policy'))) { hasSecurityHeaders = true; break; } } } if (!hasSecurityHeaders) { issues.push({ severity: 'info', type: 'missing_security_headers', message: 'No security headers configured', resolution: 'Add security headers in next.config.js', example: ` async headers() { return [ { source: '/:path*', headers: [ { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-XSS-Protection', value: '1; mode=block' } ] } ] }` }); } return issues; } async checkDependencies(projectPath) { const issues = []; try { const packageJson = await fs.readJson(path.join(projectPath, 'package.json')); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for known vulnerable versions const vulnerableDeps = { 'node-fetch': { vulnerable: '<2.6.7', reason: 'Security vulnerability in older versions' }, 'axios': { vulnerable: '<0.21.2', reason: 'SSRF vulnerability' } }; for (const [dep, info] of Object.entries(vulnerableDeps)) { if (allDeps[dep]) { const version = allDeps[dep].replace(/[\^~]/, ''); // Simple version comparison (would need semver for production) if (version < info.vulnerable.replace('<', '')) { issues.push({ severity: 'warning', type: 'vulnerable_dependency', dependency: dep, currentVersion: version, message: `${dep}@${version} - ${info.reason}`, resolution: `Update ${dep} to latest version` }); } } } } catch (error) { // Ignore if package.json doesn't exist } return issues; } async getSourceFiles(projectPath) { const patterns = ['**/*.{js,jsx,ts,tsx,json}']; const ignorePatterns = [ 'node_modules/**', '.next/**', 'build/**', 'dist/**', '.git/**', '**/*.min.js' ]; const files = []; for (const pattern of patterns) { const matches = await glob(pattern, { cwd: projectPath, ignore: ignorePatterns, absolute: true }); files.push(...matches); } return files; } generateSecurityReport(vulnerabilities) { console.log(chalk.bold('\nšŸ”’ Security Scan Report\n')); if (vulnerabilities.length === 0) { console.log(chalk.green('āœ… No security issues found!')); return; } const bySeverity = { critical: vulnerabilities.filter(v => v.severity === 'critical'), warning: vulnerabilities.filter(v => v.severity === 'warning'), info: vulnerabilities.filter(v => v.severity === 'info') }; // Summary console.log(chalk.bold('Summary:')); console.log(` ${chalk.red(`Critical: ${bySeverity.critical.length}`)}`); console.log(` ${chalk.yellow(`Warnings: ${bySeverity.warning.length}`)}`); console.log(` ${chalk.blue(`Info: ${bySeverity.info.length}`)}\n`); // Details for (const severity of ['critical', 'warning', 'info']) { const issues = bySeverity[severity]; if (issues.length === 0) continue; const color = severity === 'critical' ? 'red' : severity === 'warning' ? 'yellow' : 'blue'; console.log(chalk[color].bold(`\n${severity.toUpperCase()} Issues:`)); for (const issue of issues) { console.log(`\n ${chalk[color]('ā—')} ${issue.message}`); if (issue.file) { console.log(` File: ${issue.file}`); } if (issue.files) { console.log(` Files: ${issue.files.join(', ')}`); } if (issue.resolution) { console.log(` ${chalk.green('→')} ${issue.resolution}`); } if (issue.example) { console.log(chalk.gray(` Example:\n${issue.example}`)); } } } console.log(chalk.cyan('\nšŸ’” Run "npx embedia secure --fix" to attempt automatic fixes\n')); } } module.exports = SecurityScanner;