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
text/typescript
/**
* 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';
}
}