/** * Vulnerability Detector * Detects vulnerabilities, supply chain risks, and malicious packages */ import { Dependency, DependencyVulnerability, DependencyRiskCategory, DependencyRecommendation, CVEInfo, SupplyChainRisk, MalwareIndicator, TyposquattingCandidate } from '../types'; import { Severity, SecurityStandard } from '../../types'; import { generateId } from '../../utils'; import { getCVEsForPackage } from '../database/cveDatabase'; import { getMaliciousPackage, isDeprecatedPackage, getPopularPackages } from '../database/maliciousPackages'; import { getStandardsForDependencyRisk } from './securityStandards'; import { logger } from '../../utils/logger'; /** * Vulnerability Detector Class * Detects various security issues in dependencies */ export class VulnerabilityDetector { /** * Analyze a dependency for vulnerabilities and risks */ async analyzeDependency(dependency: Dependency): Promise { const vulnerabilities: DependencyVulnerability[] = []; // Check for known malicious packages const maliciousCheck = await this.checkMaliciousPackage(dependency); if (maliciousCheck) { vulnerabilities.push(maliciousCheck); } // Check for CVEs const cveVulns = await this.checkCVEs(dependency); vulnerabilities.push(...cveVulns); // Check for typosquatting const typosquatCheck = await this.checkTyposquatting(dependency); if (typosquatCheck) { vulnerabilities.push(typosquatCheck); } // Check for deprecated packages const deprecatedCheck = await this.checkDeprecated(dependency); if (deprecatedCheck) { vulnerabilities.push(deprecatedCheck); } // Check for supply chain risks const supplyChainRisks = await this.checkSupplyChainRisks(dependency); vulnerabilities.push(...supplyChainRisks); return vulnerabilities; } /** * Check if package is known malicious */ private async checkMaliciousPackage(dependency: Dependency): Promise { const malicious = getMaliciousPackage(dependency.name, dependency.ecosystem); if (!malicious) return null; // Check if affected version if (malicious.affectedVersions && malicious.affectedVersions !== '*') { const version = dependency.resolvedVersion || dependency.version; const affectedVersions = malicious.affectedVersions.split(',').map(v => v.trim()); if (!affectedVersions.some(v => v === version || v === '*')) { return null; } } logger.debug(`[VulnDetector] Malicious package detected: ${dependency.name}`); return { id: generateId(), dependency, severity: Severity.CRITICAL, category: DependencyRiskCategory.MALICIOUS, title: `Known Malicious Package: ${dependency.name}`, description: malicious.description, malwareIndicators: malicious.indicators, standards: getStandardsForDependencyRisk(DependencyRiskCategory.MALICIOUS), recommendation: DependencyRecommendation.REMOVE, recommendationDetails: `Immediately remove ${dependency.name} from your project. This package has been reported as malicious. ${malicious.references.length > 0 ? 'See references for more information.' : ''}`, confidence: 100, timestamp: new Date() }; } /** * Check for known CVEs */ private async checkCVEs(dependency: Dependency): Promise { const vulnerabilities: DependencyVulnerability[] = []; const version = dependency.resolvedVersion || dependency.version; // Skip if no version specified if (!version || version === '*') { return vulnerabilities; } const cves = getCVEsForPackage(dependency.name, dependency.ecosystem, version); for (const cve of cves) { logger.debug(`[VulnDetector] CVE detected: ${cve.id} in ${dependency.name}@${version}`); vulnerabilities.push({ id: generateId(), dependency, severity: cve.severity, category: DependencyRiskCategory.VULNERABILITY, title: `${cve.id}: ${dependency.name}`, description: cve.description, cve, standards: getStandardsForDependencyRisk(DependencyRiskCategory.VULNERABILITY, cve.cwes), recommendation: cve.fixedVersion ? DependencyRecommendation.UPGRADE : DependencyRecommendation.REVIEW, recommendationDetails: cve.fixedVersion ? `Upgrade ${dependency.name} to version ${cve.fixedVersion} or later to fix ${cve.id}.` : `Review the usage of ${dependency.name} and consider alternative packages. No fixed version available.`, confidence: 95, timestamp: new Date() }); } return vulnerabilities; } /** * Check for typosquatting */ private async checkTyposquatting(dependency: Dependency): Promise { const popularPackages = getPopularPackages(dependency.ecosystem); const candidates = this.findTyposquatCandidates(dependency.name, popularPackages); if (candidates.length === 0) return null; const bestMatch = candidates[0]; // Only flag if similarity is high enough (potential typosquat) if (bestMatch.similarityScore < 80) return null; logger.debug(`[VulnDetector] Potential typosquatting: ${dependency.name} similar to ${bestMatch.legitimatePackage}`); return { id: generateId(), dependency, severity: Severity.HIGH, category: DependencyRiskCategory.SUPPLY_CHAIN, title: `Potential Typosquatting: ${dependency.name}`, description: `The package "${dependency.name}" is very similar to the popular package "${bestMatch.legitimatePackage}" (${bestMatch.similarityScore}% similarity). This could be a typosquatting attempt.`, supplyChainRisks: [SupplyChainRisk.TYPOSQUATTING], standards: getStandardsForDependencyRisk(DependencyRiskCategory.SUPPLY_CHAIN), recommendation: DependencyRecommendation.REVIEW, recommendationDetails: `Verify that "${dependency.name}" is the intended package and not a typosquat of "${bestMatch.legitimatePackage}". If this is a mistake, replace with the correct package.`, confidence: bestMatch.similarityScore, timestamp: new Date() }; } /** * Find typosquatting candidates */ private findTyposquatCandidates(name: string, popularPackages: string[]): TyposquattingCandidate[] { const candidates: TyposquattingCandidate[] = []; const normalizedName = name.toLowerCase(); for (const popular of popularPackages) { const normalizedPopular = popular.toLowerCase(); // Skip if same package if (normalizedName === normalizedPopular) continue; const similarity = this.calculateSimilarity(normalizedName, normalizedPopular); if (similarity >= 70) { const typosquatType = this.detectTyposquatType(normalizedName, normalizedPopular); candidates.push({ suspiciousName: name, legitimatePackage: popular, similarityScore: similarity, typosquatType }); } } // Sort by similarity score descending return candidates.sort((a, b) => b.similarityScore - a.similarityScore); } /** * Calculate string similarity (Levenshtein distance based) */ private calculateSimilarity(a: string, b: string): number { const distance = this.levenshteinDistance(a, b); const maxLength = Math.max(a.length, b.length); return Math.round((1 - distance / maxLength) * 100); } /** * Levenshtein distance algorithm */ private levenshteinDistance(a: string, b: string): number { const matrix: number[][] = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[b.length][a.length]; } /** * Detect type of typosquat */ private detectTyposquatType(suspicious: string, legitimate: string): TyposquattingCandidate['typosquatType'] { const lenDiff = Math.abs(suspicious.length - legitimate.length); if (lenDiff === 1) { if (suspicious.length > legitimate.length) { return 'extra_char'; } else { return 'missing_char'; } } if (lenDiff === 0) { // Check for homograph (e.g., l vs 1, o vs 0) const homographs = [['l', '1'], ['o', '0'], ['i', 'l'], ['rn', 'm']]; for (const [a, b] of homographs) { if (suspicious.includes(a) && legitimate.includes(b)) { return 'homograph'; } if (suspicious.includes(b) && legitimate.includes(a)) { return 'homograph'; } } return 'character_swap'; } return 'bit_flip'; } /** * Check for deprecated packages */ private async checkDeprecated(dependency: Dependency): Promise { const { deprecated, info } = isDeprecatedPackage(dependency.name); if (!deprecated || !info) return null; // Check ecosystem match if (info.ecosystem !== dependency.ecosystem) return null; logger.debug(`[VulnDetector] Deprecated package: ${dependency.name}`); return { id: generateId(), dependency, severity: Severity.LOW, category: DependencyRiskCategory.OUTDATED, title: `Deprecated Package: ${dependency.name}`, description: `The package "${dependency.name}" is deprecated. ${info.reason}`, standards: getStandardsForDependencyRisk(DependencyRiskCategory.OUTDATED), recommendation: info.replacement ? DependencyRecommendation.REPLACE : DependencyRecommendation.REVIEW, recommendationDetails: info.replacement ? `Replace ${dependency.name} with ${info.replacement}.` : `Review usage of ${dependency.name} and consider alternatives.`, confidence: 90, timestamp: new Date() }; } /** * Check for supply chain risks */ private async checkSupplyChainRisks(dependency: Dependency): Promise { const vulnerabilities: DependencyVulnerability[] = []; const risks: SupplyChainRisk[] = []; // Check for suspicious version patterns const version = dependency.resolvedVersion || dependency.version; // Flag 0.0.x versions as potentially new/untested if (version && /^0\.0\.\d+/.test(version)) { risks.push(SupplyChainRisk.NEW_PACKAGE); } // Flag packages with very recent versions (could be suspicious release) // Note: This would need actual publish date data in production if (risks.length > 0) { vulnerabilities.push({ id: generateId(), dependency, severity: Severity.INFO, category: DependencyRiskCategory.SUPPLY_CHAIN, title: `Supply Chain Risk: ${dependency.name}`, description: `The package "${dependency.name}" has potential supply chain risks: ${risks.join(', ')}`, supplyChainRisks: risks, standards: getStandardsForDependencyRisk(DependencyRiskCategory.SUPPLY_CHAIN), recommendation: DependencyRecommendation.MONITOR, recommendationDetails: `Monitor ${dependency.name} for any suspicious activity or updates. Consider pinning to a specific trusted version.`, confidence: 60, timestamp: new Date() }); } return vulnerabilities; } } export default VulnerabilityDetector;