UNPKG

pury

Version:

🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis

369 lines 16.1 kB
import { FindingType, Severity } from '../types/index.js'; import { logger } from '../utils/logger.js'; export class DependencyAnalyzer { vulnerabilityDatabase = new Map(); suspiciousPackages = new Set(); constructor() { this.loadKnownVulnerabilities(); this.loadSuspiciousPackages(); } async analyze(files, onProgress) { const startTime = Date.now(); const findings = []; let filesAnalyzed = 0; // Find package.json files const packageFiles = files.filter(file => file.relativePath.endsWith('package.json') || file.relativePath.endsWith('composer.json') || file.relativePath.endsWith('requirements.txt') || file.relativePath.endsWith('Gemfile') || file.relativePath.endsWith('go.mod')); // Also check for lock files and analyze their structure const lockFiles = files.filter(file => file.relativePath.includes('package-lock.json') || file.relativePath.includes('yarn.lock') || file.relativePath.includes('composer.lock')); const allDependencyFiles = [...packageFiles, ...lockFiles]; for (let i = 0; i < allDependencyFiles.length; i++) { const file = allDependencyFiles[i]; onProgress?.(i + 1, allDependencyFiles.length, file.path); try { let fileFindings = []; if (packageFiles.includes(file)) { fileFindings = await this.analyzePackageFile(file); } else if (lockFiles.includes(file)) { fileFindings = await this.analyzeLockFile(file); } findings.push(...fileFindings); filesAnalyzed++; } catch (error) { logger.warn(`Failed to analyze dependency file ${file.path}: ${error.message}`); } } const processingTime = Date.now() - startTime; logger.debug(`Dependency analysis completed in ${processingTime}ms`); return { findings, processingTime, filesAnalyzed }; } async analyzePackageFile(file) { const findings = []; try { if (file.relativePath.endsWith('package.json')) { const packageData = JSON.parse(file.content); findings.push(...this.analyzeNodePackage(packageData, file)); } else if (file.relativePath.endsWith('requirements.txt')) { findings.push(...this.analyzePythonRequirements(file)); } else if (file.relativePath.endsWith('composer.json')) { findings.push(...this.analyzeComposerPackage(file)); } } catch (error) { findings.push({ id: `dep-parse-${Date.now()}`, type: FindingType.VULNERABILITY, severity: Severity.LOW, title: 'Package File Parse Error', description: `Failed to parse package file: ${error.message}`, file: file.path, suggestion: 'Ensure the package file has valid JSON/format syntax' }); } return findings; } analyzeNodePackage(packageData, file) { const findings = []; // Check all dependencies const allDeps = { ...packageData.dependencies, ...packageData.devDependencies }; for (const [packageName, version] of Object.entries(allDeps || {})) { // Check for suspicious packages if (this.suspiciousPackages.has(packageName)) { findings.push({ id: `dep-suspicious-${Date.now()}-${packageName}`, type: FindingType.MALWARE, severity: Severity.HIGH, title: 'Suspicious Package Detected', description: `Package "${packageName}" is known to be suspicious or malicious`, file: file.path, evidence: `"${packageName}": "${version}"`, suggestion: 'Remove this package and find a trusted alternative', references: ['https://github.com/advisories'] }); } // Check for typosquatting const typoSquattingResult = this.checkTypoSquatting(packageName); if (typoSquattingResult) { findings.push({ id: `dep-typo-${Date.now()}-${packageName}`, type: FindingType.MALWARE, severity: Severity.MEDIUM, title: 'Potential Typosquatting', description: `Package "${packageName}" may be a typosquatted version of "${typoSquattingResult.legitimate}"`, file: file.path, evidence: `"${packageName}": "${version}"`, suggestion: `Consider using the legitimate package "${typoSquattingResult.legitimate}" instead` }); } // Check for known vulnerabilities const vulnerabilities = this.vulnerabilityDatabase.get(packageName); if (vulnerabilities) { for (const vuln of vulnerabilities) { if (this.isVersionAffected(version, vuln.affectedVersions)) { findings.push({ id: `dep-vuln-${vuln.id}`, type: FindingType.VULNERABILITY, severity: this.mapSeverity(vuln.severity), title: `Known Vulnerability: ${vuln.title}`, description: vuln.description, file: file.path, evidence: `"${packageName}": "${version}"`, suggestion: vuln.patchedVersions ? `Update to version ${vuln.patchedVersions} or later` : 'Update to the latest version or find an alternative', references: vuln.reference ? [vuln.reference] : [] }); } } } // Check for outdated packages (simplified) if (this.isLikelyOutdated(version)) { findings.push({ id: `dep-outdated-${Date.now()}-${packageName}`, type: FindingType.VULNERABILITY, severity: Severity.LOW, title: 'Potentially Outdated Package', description: `Package "${packageName}" appears to use an old version format`, file: file.path, evidence: `"${packageName}": "${version}"`, suggestion: 'Check for updates and security patches for this package' }); } } // Check for missing security configurations if (!packageData.dependencies?.helmet && !packageData.devDependencies?.helmet) { // This is just an example - in practice, you'd have more sophisticated checks if (Object.keys(packageData.dependencies || {}).some(dep => ['express', 'koa', 'fastify'].includes(dep))) { findings.push({ id: `dep-security-${Date.now()}`, type: FindingType.VULNERABILITY, severity: Severity.LOW, title: 'Missing Security Middleware', description: 'Web application detected without security middleware like Helmet', file: file.path, suggestion: 'Consider adding security middleware to protect against common vulnerabilities' }); } } return findings; } analyzePythonRequirements(file) { const findings = []; const lines = file.content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]?.trim(); if (!line || line.startsWith('#')) continue; const match = /^([a-zA-Z0-9_-]+)([>=<!=]+)(.+)$/.exec(line); if (match) { const [, packageName] = match; if (packageName && this.suspiciousPackages.has(packageName)) { findings.push({ id: `dep-suspicious-py-${Date.now()}-${packageName}`, type: FindingType.MALWARE, severity: Severity.HIGH, title: 'Suspicious Python Package', description: `Package "${packageName}" is known to be suspicious`, file: file.path, line: i + 1, evidence: line, suggestion: 'Remove this package and find a trusted alternative' }); } } } return findings; } analyzeComposerPackage(_file) { const findings = []; // Similar analysis for PHP Composer packages // Implementation would follow similar patterns to Node.js analysis return findings; } async analyzeLockFile(file) { const findings = []; try { if (file.relativePath.includes('package-lock.json')) { const lockData = JSON.parse(file.content); // Analyze lock file for integrity issues findings.push(...this.analyzePackageLockIntegrity(lockData, file)); } } catch (error) { findings.push({ id: `lock-parse-${Date.now()}`, type: FindingType.VULNERABILITY, severity: Severity.LOW, title: 'Lock File Parse Error', description: `Failed to parse lock file: ${error.message}`, file: file.path, suggestion: 'Regenerate the lock file or check for corruption' }); } return findings; } analyzePackageLockIntegrity(lockData, file) { const findings = []; // Check for missing integrity hashes (simplified check) if (lockData.packages) { let packagesWithoutIntegrity = 0; let totalPackages = 0; for (const [packagePath, packageInfo] of Object.entries(lockData.packages)) { if (packagePath === '') continue; // Skip root package totalPackages++; if (!packageInfo || !packageInfo?.integrity) { packagesWithoutIntegrity++; } } if (packagesWithoutIntegrity > 0) { findings.push({ id: `lock-integrity-${Date.now()}`, type: FindingType.VULNERABILITY, severity: Severity.MEDIUM, title: 'Missing Package Integrity Hashes', description: `${packagesWithoutIntegrity} out of ${totalPackages} packages are missing integrity hashes`, file: file.path, suggestion: 'Regenerate package-lock.json with npm install to ensure integrity hashes' }); } } return findings; } checkTypoSquatting(packageName) { // Simple typosquatting detection - in practice, this would use a comprehensive database const commonPackages = [ 'express', 'react', 'lodash', 'axios', 'moment', 'redux', 'webpack', 'babel', 'eslint', 'prettier', 'typescript', 'jest', 'mocha', 'chai' ]; for (const legitimate of commonPackages) { if (this.isLikelyTypo(packageName, legitimate)) { return { legitimate }; } } return null; } isLikelyTypo(test, target) { if (test === target) return false; // Simple Levenshtein distance check const distance = this.levenshteinDistance(test, target); return distance === 1 && test.length >= 3; // Allow 1 character difference for packages >= 3 chars } levenshteinDistance(str1, str2) { const matrix = Array.from({ length: str2.length + 1 }, (_, i) => [i]); matrix[0] = Array.from({ length: str1.length + 1 }, (_, i) => i); for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { const cost = str1[j - 1] === str2[i - 1] ? 0 : 1; matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion matrix[i][j - 1] + 1, // insertion matrix[i - 1][j - 1] + cost // substitution ); } } return matrix[str2.length][str1.length]; } isVersionAffected(installedVersion, affectedVersions) { // Simplified version comparison - in practice, use semver library // This would need proper semver range checking return affectedVersions.includes(installedVersion.replace(/[^0-9.]/g, '')); } isLikelyOutdated(version) { // Simple check for obviously old versions const oldVersionPatterns = [ /^0\./, // Version starting with 0.x /^1\.[0-5]\./, // Very old 1.x versions /\d{4}$/ // Versions ending with year (like 2018) ]; return oldVersionPatterns.some(pattern => pattern.test(version)); } mapSeverity(severity) { switch (severity.toLowerCase()) { case 'critical': return Severity.CRITICAL; case 'high': return Severity.HIGH; case 'moderate': return Severity.MEDIUM; case 'low': return Severity.LOW; default: return Severity.MEDIUM; } } loadKnownVulnerabilities() { // In a real implementation, this would load from a vulnerability database // For now, we'll add some example vulnerabilities this.vulnerabilityDatabase.set('lodash', [ { id: 'CVE-2020-8203', severity: 'high', title: 'Prototype Pollution', description: 'Lodash versions prior to 4.17.19 are vulnerable to prototype pollution', affectedVersions: '<4.17.19', patchedVersions: '>=4.17.19', reference: 'https://nvd.nist.gov/vuln/detail/CVE-2020-8203' } ]); this.vulnerabilityDatabase.set('axios', [ { id: 'CVE-2020-28168', severity: 'moderate', title: 'Server-Side Request Forgery', description: 'Axios versions prior to 0.21.1 are vulnerable to SSRF', affectedVersions: '<0.21.1', patchedVersions: '>=0.21.1', reference: 'https://nvd.nist.gov/vuln/detail/CVE-2020-28168' } ]); } loadSuspiciousPackages() { // In practice, this would load from a curated list of known malicious packages const suspiciousList = [ 'event-stream', // Historical malicious package 'getcookies', // Typosquatting example 'http-fetch', // Suspicious package example 'node-env' // Another example ]; this.suspiciousPackages = new Set(suspiciousList); } addSuspiciousPackage(packageName) { this.suspiciousPackages.add(packageName); } addVulnerability(packageName, vulnerability) { if (!this.vulnerabilityDatabase.has(packageName)) { this.vulnerabilityDatabase.set(packageName, []); } this.vulnerabilityDatabase.get(packageName).push(vulnerability); } } //# sourceMappingURL=dependency-analyzer.js.map