embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
280 lines (238 loc) ⢠8.5 kB
JavaScript
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;