UNPKG

vuln-scanner-cli

Version:

A comprehensive dependency vulnerability scanner for Node.js applications

225 lines (195 loc) 7.14 kB
const axios = require('axios'); const fs = require('fs-extra'); const path = require('path'); class VulnerabilityDatabase { constructor() { this.cacheDir = path.join(__dirname, '../cache'); this.cacheFile = path.join(this.cacheDir, 'vuln-cache.json'); this.cacheExpiry = 24 * 60 * 60 * 1000; // 24 hours } async ensureCacheDir() { await fs.ensureDir(this.cacheDir); } async getCachedData() { try { if (await fs.pathExists(this.cacheFile)) { const cacheData = await fs.readJson(this.cacheFile); const now = Date.now(); if (now - cacheData.timestamp < this.cacheExpiry) { return cacheData.data; } } } catch (error) { console.warn('Failed to read cache:', error.message); } return null; } async setCachedData(data) { try { await this.ensureCacheDir(); await fs.writeJson(this.cacheFile, { timestamp: Date.now(), data }); } catch (error) { console.warn('Failed to write cache:', error.message); } } async fetchNpmAuditData(dependencies) { try { // Create a minimal package.json for npm audit const auditPayload = { name: 'temp-audit', version: '1.0.0', dependencies: {} }; // Add dependencies to audit payload dependencies.forEach(dep => { if (dep.isDirect) { auditPayload.dependencies[dep.name] = dep.installedVersion; } }); const response = await axios.post('https://registry.npmjs.org/-/npm/v1/security/audits', auditPayload, { headers: { 'Content-Type': 'application/json', 'User-Agent': 'vuln-scanner-cli/1.0.0' }, timeout: 30000 }); return response.data; } catch (error) { if (error.response && error.response.status === 400) { // No vulnerabilities found return { vulnerabilities: {}, metadata: { vulnerabilities: { total: 0 } } }; } throw new Error(`Failed to fetch npm audit data: ${error.message}`); } } async fetchOSVData(packageName, version) { try { const response = await axios.post('https://api.osv.dev/v1/query', { package: { name: packageName, ecosystem: 'npm' }, version: version }, { headers: { 'Content-Type': 'application/json' }, timeout: 10000 }); return response.data.vulns || []; } catch (error) { console.warn(`Failed to fetch OSV data for ${packageName}: ${error.message}`); return []; } } async getVulnerabilities(dependencies) { console.log('🔍 Fetching vulnerability data...'); try { // Try npm audit first const npmAuditData = await this.fetchNpmAuditData(dependencies); const vulnerabilities = []; if (npmAuditData.vulnerabilities) { Object.entries(npmAuditData.vulnerabilities).forEach(([vulnId, vuln]) => { vulnerabilities.push({ id: vulnId, title: vuln.title, severity: vuln.severity, vulnerable_versions: vuln.vulnerable_versions, patched_versions: vuln.patched_versions, module_name: vuln.module_name, overview: vuln.overview, recommendation: vuln.recommendation, references: vuln.references, source: 'npm' }); }); } // Supplement with OSV data for critical packages const criticalPackages = dependencies.filter(dep => dep.isDirect).slice(0, 10); for (const dep of criticalPackages) { try { const osvVulns = await this.fetchOSVData(dep.name, dep.installedVersion); osvVulns.forEach(vuln => { if (!vulnerabilities.find(v => v.id === vuln.id)) { vulnerabilities.push({ id: vuln.id, title: vuln.summary || 'Security vulnerability', severity: this.mapOSVSeverity(vuln.severity), vulnerable_versions: vuln.affected?.[0]?.ranges?.[0]?.events?.map(e => e.introduced || e.fixed).join(', ') || 'unknown', patched_versions: vuln.affected?.[0]?.ranges?.[0]?.events?.find(e => e.fixed)?.fixed || 'unknown', module_name: dep.name, overview: vuln.details || vuln.summary, recommendation: 'Update to a patched version', references: vuln.references?.map(r => r.url) || [], source: 'osv' }); } }); } catch (error) { // Continue with other packages if one fails continue; } } return vulnerabilities; } catch (error) { throw new Error(`Failed to fetch vulnerability data: ${error.message}`); } } mapOSVSeverity(osvSeverity) { if (!osvSeverity || !osvSeverity[0]) return 'moderate'; const score = osvSeverity[0].score; if (score >= 9.0) return 'critical'; if (score >= 7.0) return 'high'; if (score >= 4.0) return 'moderate'; return 'low'; } async getPackageAlternatives(packageName) { try { // Search for similar packages on npm const searchResponse = await axios.get(`https://registry.npmjs.org/-/v1/search`, { params: { text: `keywords:${packageName}`, size: 10 }, timeout: 10000 }); const alternatives = []; for (const pkg of searchResponse.data.objects) { if (pkg.package.name === packageName) continue; // Get additional package info try { const pkgResponse = await axios.get(`https://registry.npmjs.org/${pkg.package.name}`, { timeout: 5000 }); const pkgData = pkgResponse.data; const latestVersion = pkgData['dist-tags']?.latest; alternatives.push({ name: pkg.package.name, description: pkg.package.description || 'No description available', version: latestVersion, downloads: pkg.package.links?.npm || 0, stars: pkg.score?.detail?.popularity || 0, quality: pkg.score?.detail?.quality || 0, maintenance: pkg.score?.detail?.maintenance || 0, homepage: pkgData.homepage, repository: pkgData.repository?.url }); } catch (error) { // Skip this package if we can't get details continue; } } // Sort by quality and popularity return alternatives .sort((a, b) => (b.quality + b.stars) - (a.quality + a.stars)) .slice(0, 5); } catch (error) { console.warn(`Failed to fetch alternatives for ${packageName}: ${error.message}`); return []; } } } module.exports = VulnerabilityDatabase;