vuln-scanner-cli
Version:
A comprehensive dependency vulnerability scanner for Node.js applications
225 lines (195 loc) • 7.14 kB
JavaScript
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;