UNPKG

recoder-code

Version:

🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!

609 lines (512 loc) • 19.1 kB
/** * SecurityService * Handles security scanning, vulnerability detection, and threat analysis */ import { Config } from '../config'; import * as crypto from 'crypto'; import * as tar from 'tar'; import * as fs from 'fs-extra'; import * as path from 'path'; import { PackageVersion } from '../entities/PackageVersion'; export interface SecurityScanResult { status: 'clean' | 'warning' | 'critical'; vulnerabilities: Vulnerability[]; threats: Threat[]; malware_detected: boolean; risk_score: number; scan_duration: number; scanner_version: string; } export interface Vulnerability { id: string; severity: 'low' | 'medium' | 'high' | 'critical'; title: string; description: string; cve?: string; cwe?: string; affected_versions: string[]; patched_versions: string[]; recommendation: string; references: string[]; } export interface Threat { type: 'malware' | 'backdoor' | 'typosquatting' | 'suspicious_code' | 'data_exfiltration'; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; evidence: string[]; confidence: number; } export interface ScanOptions { deep_scan?: boolean; check_dependencies?: boolean; malware_detection?: boolean; license_check?: boolean; timeout?: number; } export class SecurityService { private readonly scannerVersion = '1.0.0'; private readonly logger = { log: (message: string) => console.log(`[SecurityService] ${message}`), warn: (message: string, error?: any) => console.warn(`[SecurityService] ${message}`, error), error: (message: string, error?: any) => console.error(`[SecurityService] ${message}`, error) }; constructor(private config?: Config) {} async scanTarball(tarballBuffer: Buffer): Promise<{ passed: boolean; issues: string[] }> { // Simple security scan - just return passed for now return { passed: true, issues: [] }; } async scanPackage( packageBuffer: Buffer, packageVersion: PackageVersion, options: ScanOptions = {} ): Promise<SecurityScanResult> { const startTime = Date.now(); this.logger.log(`Starting security scan for ${packageVersion.package?.name}@${packageVersion.version}`); try { const { deep_scan = true, check_dependencies = true, malware_detection = true, license_check = true, timeout = 300000 // 5 minutes } = options; const results: SecurityScanResult = { status: 'clean', vulnerabilities: [], threats: [], malware_detected: false, risk_score: 0, scan_duration: 0, scanner_version: this.scannerVersion }; // Create temporary directory for scanning const tempDir = path.join('/tmp', `scan_${crypto.randomUUID()}`); await fs.ensureDir(tempDir); try { // Extract package await this.extractPackage(packageBuffer, tempDir); // Run parallel scans with timeout const scanPromises = [ this.scanForVulnerabilities(tempDir, packageVersion), malware_detection ? this.scanForMalware(tempDir) : Promise.resolve([]), this.scanForSuspiciousPatterns(tempDir), check_dependencies ? this.scanDependencies(tempDir, packageVersion) : Promise.resolve([]), license_check ? this.scanLicenses(tempDir) : Promise.resolve([]) ]; // Helper to add timeout to Promise.all async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> { return new Promise<T>((resolve, reject) => { const timer = setTimeout(() => reject(new Error('Scan timeout')), ms); promise.then( (val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); } ); }); } const [ vulnerabilities, malwareThreats, suspiciousThreats, dependencyVulns, licenseIssues ] = await withTimeout(Promise.all(scanPromises), timeout) as [ Vulnerability[], Threat[], Threat[], Vulnerability[], Threat[] ]; // Combine results results.vulnerabilities = [...vulnerabilities, ...dependencyVulns]; results.threats = [...malwareThreats, ...suspiciousThreats, ...licenseIssues]; results.malware_detected = malwareThreats.some(t => t.type === 'malware'); // Calculate risk score results.risk_score = this.calculateRiskScore(results); // Determine overall status results.status = this.determineStatus(results); results.scan_duration = Date.now() - startTime; this.logger.log( `Security scan completed for ${packageVersion.package?.name}@${packageVersion.version}: ` + `${results.status} (${results.vulnerabilities.length} vulns, ${results.threats.length} threats)` ); return results; } finally { // Cleanup await fs.remove(tempDir).catch(err => this.logger.warn(`Failed to cleanup temp dir: ${err.message}`) ); } } catch (error) { const err = error as Error; this.logger.error(`Security scan failed: ${err.message}`, err.stack); return { status: 'critical', vulnerabilities: [], threats: [{ type: 'suspicious_code', severity: 'critical', description: `Scan failed: ${error instanceof Error ? error.message : String(error)}`, evidence: [], confidence: 0.5 }], malware_detected: false, risk_score: 100, scan_duration: Date.now() - startTime, scanner_version: this.scannerVersion }; } } private async extractPackage(packageBuffer: Buffer, extractPath: string): Promise<void> { const tarStream = tar.extract({ cwd: extractPath, strip: 1, filter: (path) => { // Security: Prevent path traversal const normalizedPath = path.normalize(path); return !normalizedPath.includes('..') && normalizedPath.length < 255; } }); return new Promise((resolve, reject) => { tarStream.on('error', reject); tarStream.on('end', resolve); tarStream.write(packageBuffer); tarStream.end(); }); } private async scanForVulnerabilities( extractPath: string, packageVersion: PackageVersion ): Promise<Vulnerability[]> { const vulnerabilities: Vulnerability[] = []; try { // Check known vulnerability databases const packageJson = await this.readPackageJson(extractPath); if (!packageJson) return vulnerabilities; // Simulate vulnerability checking against known databases // In production, this would integrate with npm audit, Snyk, or similar services // Check for outdated dependencies if (packageVersion.dependencies) { for (const [depName, depVersion] of Object.entries(packageVersion.dependencies)) { const vulns = await this.checkDependencyVulnerabilities(depName, depVersion); vulnerabilities.push(...vulns); } } // Check for known vulnerable patterns in code const codeVulns = await this.scanCodeForVulnerabilities(extractPath); vulnerabilities.push(...codeVulns); } catch (error) { this.logger.warn(`Vulnerability scan failed: ${(error as Error).message}`); } return vulnerabilities; } private async scanForMalware(extractPath: string): Promise<Threat[]> { const threats: Threat[] = []; try { // Scan for malicious patterns const files = await this.getAllFiles(extractPath); for (const file of files) { const content = await fs.readFile(file, 'utf8').catch(() => null); if (!content) continue; // Check for suspicious patterns const malwarePatterns = [ /eval\s*\(\s*(?:atob|Buffer\.from)/gi, // Base64 eval /child_process\s*\.\s*exec\s*\(\s*['"`][\w\s\/\-\\]*rm\s+\-rf/gi, // Destructive commands /crypto\.createCipher.*password/gi, // Suspicious encryption /require\s*\(\s*['"`]http[s]?:\/\//gi, // Remote requires /\.download\s*\(\s*['"`]http/gi, // File downloads /process\.env\[['"`]([A-Z_]+)['"`]\]/gi // Environment variable access ]; for (const pattern of malwarePatterns) { const matches = content.match(pattern); if (matches) { threats.push({ type: 'malware', severity: 'high', description: `Suspicious pattern detected in ${path.relative(extractPath, file)}`, evidence: matches.slice(0, 3), // Limit evidence confidence: 0.7 }); } } } } catch (error) { this.logger.warn(`Malware scan failed: ${(error as Error).message}`); } return threats; } private async scanForSuspiciousPatterns(extractPath: string): Promise<Threat[]> { const threats: Threat[] = []; try { const packageJson = await this.readPackageJson(extractPath); if (!packageJson) return threats; // Check for typosquatting if (this.isLikelyTyposquat(packageJson.name)) { threats.push({ type: 'typosquatting', severity: 'medium', description: 'Package name resembles popular package (potential typosquatting)', evidence: [packageJson.name], confidence: 0.6 }); } // Check for suspicious scripts if (packageJson.scripts) { for (const [scriptName, script] of Object.entries(packageJson.scripts)) { if (this.isSuspiciousScript(script as string)) { threats.push({ type: 'suspicious_code', severity: 'medium', description: `Suspicious script detected: ${scriptName}`, evidence: [script as string], confidence: 0.5 }); } } } // Check for data exfiltration patterns const files = await this.getAllFiles(extractPath); for (const file of files) { const content = await fs.readFile(file, 'utf8').catch(() => null); if (!content) continue; if (this.hasDataExfiltrationPatterns(content)) { threats.push({ type: 'data_exfiltration', severity: 'high', description: `Potential data exfiltration code in ${path.relative(extractPath, file)}`, evidence: ['Sensitive data collection patterns detected'], confidence: 0.6 }); } } } catch (error) { this.logger.warn(`Suspicious pattern scan failed: ${(error as Error).message}`); } return threats; } private async scanDependencies( extractPath: string, packageVersion: PackageVersion ): Promise<Vulnerability[]> { const vulnerabilities: Vulnerability[] = []; try { // This would integrate with npm audit or similar tools // For now, we'll do basic checks const allDeps = { ...packageVersion.dependencies, ...packageVersion.dev_dependencies, ...packageVersion.peer_dependencies, ...packageVersion.optional_dependencies }; for (const [depName, depVersion] of Object.entries(allDeps)) { const vulns = await this.checkDependencyVulnerabilities(depName, depVersion); vulnerabilities.push(...vulns); } } catch (error) { this.logger.warn(`Dependency scan failed: ${(error as Error).message}`); } return vulnerabilities; } private async scanLicenses(extractPath: string): Promise<Threat[]> { const threats: Threat[] = []; try { const packageJson = await this.readPackageJson(extractPath); if (!packageJson) return threats; // Check for license issues if (!packageJson.license || packageJson.license === 'UNLICENSED') { threats.push({ type: 'suspicious_code', severity: 'low', description: 'Package has no license or is unlicensed', evidence: [packageJson.license || 'No license specified'], confidence: 0.3 }); } // Check for restrictive licenses const restrictiveLicenses = ['GPL-3.0', 'AGPL-3.0', 'SSPL-1.0']; if (restrictiveLicenses.includes(packageJson.license)) { threats.push({ type: 'suspicious_code', severity: 'low', description: 'Package uses restrictive license', evidence: [packageJson.license], confidence: 0.2 }); } } catch (error) { this.logger.warn(`License scan failed: ${(error as Error).message}`); } return threats; } private async checkDependencyVulnerabilities( depName: string, depVersion: string ): Promise<Vulnerability[]> { // This would integrate with vulnerability databases // For now, return empty array return []; } private async scanCodeForVulnerabilities(extractPath: string): Promise<Vulnerability[]> { const vulnerabilities: Vulnerability[] = []; try { const files = await this.getAllFiles(extractPath, ['.js', '.ts', '.json']); for (const file of files) { const content = await fs.readFile(file, 'utf8').catch(() => null); if (!content) continue; // Check for known vulnerable patterns if (content.includes('eval(') || content.includes('Function(')) { vulnerabilities.push({ id: 'code-injection', severity: 'high', title: 'Code Injection Risk', description: 'Use of eval() or Function() constructor detected', affected_versions: ['*'], patched_versions: [], recommendation: 'Remove use of eval() and Function() constructor', references: ['https://owasp.org/www-community/attacks/Code_Injection'] }); } if (content.includes('innerHTML') && !content.includes('sanitize')) { vulnerabilities.push({ id: 'xss-risk', severity: 'medium', title: 'XSS Risk', description: 'Use of innerHTML without sanitization detected', affected_versions: ['*'], patched_versions: [], recommendation: 'Use textContent or sanitize HTML content', references: ['https://owasp.org/www-community/attacks/xss/'] }); } } } catch (error) { this.logger.warn(`Code vulnerability scan failed: ${(error as Error).message}`); } return vulnerabilities; } private async readPackageJson(extractPath: string): Promise<any> { try { const packageJsonPath = path.join(extractPath, 'package.json'); const content = await fs.readFile(packageJsonPath, 'utf8'); return JSON.parse(content); } catch { return null; } } private async getAllFiles( dir: string, extensions?: string[] ): Promise<string[]> { const files: string[] = []; const items = await fs.readdir(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = await fs.stat(fullPath); if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') { files.push(...await this.getAllFiles(fullPath, extensions)); } else if (stat.isFile()) { if (!extensions || extensions.some(ext => item.endsWith(ext))) { files.push(fullPath); } } } return files; } private isLikelyTyposquat(packageName: string): boolean { const popularPackages = [ 'react', 'vue', 'angular', 'lodash', 'express', 'axios', 'moment', 'jquery', 'bootstrap', 'webpack', 'babel', 'typescript', 'eslint' ]; return popularPackages.some(popular => { const distance = this.levenshteinDistance(packageName.toLowerCase(), popular); return distance > 0 && distance <= 2 && packageName.length >= popular.length - 1; }); } private isSuspiciousScript(script: string): boolean { const suspiciousPatterns = [ /curl\s+.*\|\s*sh/, /wget\s+.*\|\s*sh/, /rm\s+-rf/, /chmod\s+\+x/, /base64\s+-d/, /eval\s*\$/ ]; return suspiciousPatterns.some(pattern => pattern.test(script)); } private hasDataExfiltrationPatterns(content: string): boolean { const patterns = [ /process\.env/g, /require\s*\(\s*['"`]os['"`]\s*\)/g, /\.hostname\(\)/g, /\.userInfo\(\)/g, /\.networkInterfaces\(\)/g ]; let suspiciousCount = 0; for (const pattern of patterns) { if (pattern.test(content)) { suspiciousCount++; } } return suspiciousCount >= 3; // Multiple indicators } private levenshteinDistance(str1: string, str2: string): number { const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min( matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + indicator ); } } return matrix[str2.length][str1.length]; } private calculateRiskScore(results: SecurityScanResult): number { let score = 0; // Vulnerability scoring for (const vuln of results.vulnerabilities) { switch (vuln.severity) { case 'critical': score += 25; break; case 'high': score += 15; break; case 'medium': score += 8; break; case 'low': score += 3; break; } } // Threat scoring for (const threat of results.threats) { const baseScore = { 'critical': 20, 'high': 12, 'medium': 6, 'low': 2 }[threat.severity]; score += baseScore * threat.confidence; } // Malware detection if (results.malware_detected) { score += 50; } return Math.min(100, Math.round(score)); } private determineStatus(results: SecurityScanResult): 'clean' | 'warning' | 'critical' { if (results.malware_detected) return 'critical'; if (results.risk_score >= 70) return 'critical'; if (results.risk_score >= 30) return 'warning'; const hasCriticalVuln = results.vulnerabilities.some(v => v.severity === 'critical'); const hasCriticalThreat = results.threats.some(t => t.severity === 'critical'); if (hasCriticalVuln || hasCriticalThreat) return 'critical'; const hasHighRisk = results.vulnerabilities.some(v => v.severity === 'high') || results.threats.some(t => t.severity === 'high'); if (hasHighRisk) return 'warning'; return 'clean'; } }