UNPKG

vulnzap-mcp

Version:

Multi-ecosystem vulnerability scanning service with MCP interface for LLMs

316 lines (272 loc) 9.82 kB
/** * Open Source Vulnerability (OSV) Database Source * * Service for querying the OSV API for vulnerability information */ import fetch from 'node-fetch'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { CONFIG } from '../core/config.js'; import { cacheData, loadCachedData } from '../utils/cache-manager.js'; import { parseVersion, compareVersions } from '../utils/version-parsers.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const CACHE_FILE = path.join(CONFIG.DATA_PATHS.CACHE_DIR, 'osv-vulnerabilities.json'); const REFRESH_INTERVAL = CONFIG.REFRESH_INTERVALS.OSV || 24 * 60 * 60 * 1000; // Default 24 hours // OSV ecosystem mapping const ECOSYSTEM_MAP = { 'npm': 'npm', 'pip': 'PyPI', 'go': 'Go', 'cargo': 'crates.io', 'maven': 'Maven', 'nuget': 'NuGet', 'composer': 'Packagist' }; /** * Open Source Vulnerability client */ export default class OsvSource { constructor(options = {}) { this.options = { apiUrl: CONFIG.SERVICE_ENDPOINTS.OSV || 'https://api.osv.dev/v1', cacheFile: options.cacheFile || CACHE_FILE, refreshInterval: options.refreshInterval || REFRESH_INTERVAL, ...options }; this.vulnerabilities = new Map(); this.lastRefreshed = 0; this.isInitialized = false; } /** * Initialize the data source */ async initialize() { try { // Create cache directory if it doesn't exist await fs.mkdir(CONFIG.DATA_PATHS.CACHE_DIR, { recursive: true }); // Load cached data if available const cachedData = await loadCachedData(this.options.cacheFile); if (cachedData) { this.vulnerabilities = new Map(Object.entries(cachedData.vulnerabilities)); this.lastRefreshed = cachedData.timestamp || 0; console.log(`Loaded ${this.vulnerabilities.size} OSV vulnerabilities from cache`); } this.isInitialized = true; return true; } catch (error) { console.error('Failed to initialize OSV source', error); return false; } } /** * Save vulnerabilities to cache file */ async saveToCache() { try { const cacheData = { timestamp: Date.now(), vulnerabilities: Object.fromEntries(this.vulnerabilities) }; await fs.writeFile(this.options.cacheFile, JSON.stringify(cacheData)); return true; } catch (error) { console.error('Failed to save OSV vulnerabilities to cache', error); return false; } } /** * Query OSV API for a specific package and version */ async queryOsvApi(packageName, version, ecosystem) { try { const osvEcosystem = ECOSYSTEM_MAP[ecosystem] || ecosystem; const requestBody = { package: { name: packageName, ecosystem: osvEcosystem }, version }; // Make OSV API request const response = await fetch(`${this.options.apiUrl}/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`OSV API error: ${response.status} - ${errorText}`); } const data = await response.json(); return data.vulns || []; } catch (error) { console.error(`Error querying OSV for ${packageName}@${version}:`, error); return []; } } /** * Determines the severity level from various sources in an OSV entry */ determineSeverity(osvVulnerability) { // OSV doesn't have a standard severity field, but may include it in database_specific // Try to get from CVSS if (osvVulnerability.severity && osvVulnerability.severity.length > 0) { for (const sev of osvVulnerability.severity) { if (sev.type === 'CVSS_V3') { const score = parseFloat(sev.score); if (score >= 9.0) return 'critical'; if (score >= 7.0) return 'high'; if (score >= 4.0) return 'medium'; if (score > 0.0) return 'low'; return 'none'; } } } // Try to get from database-specific information if (osvVulnerability.database_specific) { // GitHub Security Advisory format if (osvVulnerability.database_specific.severity) { return osvVulnerability.database_specific.severity.toLowerCase(); } // NVD format if (osvVulnerability.database_specific.cvss && osvVulnerability.database_specific.cvss.baseScore) { const score = parseFloat(osvVulnerability.database_specific.cvss.baseScore); if (score >= 9.0) return 'critical'; if (score >= 7.0) return 'high'; if (score >= 4.0) return 'medium'; if (score > 0.0) return 'low'; return 'none'; } } // Default to medium if we can't determine return 'medium'; } /** * Extract CVSS score from an OSV entry */ extractCvssScore(osvVulnerability) { // Check severity section first if (osvVulnerability.severity && osvVulnerability.severity.length > 0) { for (const sev of osvVulnerability.severity) { if (sev.type === 'CVSS_V3' && sev.score) { return parseFloat(sev.score); } } } // Check database-specific if (osvVulnerability.database_specific) { // GHSA format if (osvVulnerability.database_specific.cvss && osvVulnerability.database_specific.cvss.score) { return parseFloat(osvVulnerability.database_specific.cvss.score); } // NVD format if (osvVulnerability.database_specific.cvss && osvVulnerability.database_specific.cvss.baseScore) { return parseFloat(osvVulnerability.database_specific.cvss.baseScore); } } return null; } /** * Process OSV API results into standardized format */ processOsvResults(results, packageName, version, ecosystem) { return results .map(vuln => { // Extract severity information const severity = this.determineSeverity(vuln); const cvss = this.extractCvssScore(vuln); // Process affected versions let affectedVersions = null; if (vuln.affected && vuln.affected.length > 0) { // Join affected version ranges const ranges = []; vuln.affected.forEach(affected => { if (affected.versions) { // Direct version list affected.versions.forEach(ver => { ranges.push(`=${ver}`); }); } else if (affected.ranges) { // Version ranges affected.ranges.forEach(range => { if (range.type === 'SEMVER') { range.events.forEach((event, i, events) => { if (event.introduced && events[i+1] && events[i+1].fixed) { ranges.push(`>=${event.introduced} <${events[i+1].fixed}`); } else if (event.introduced) { ranges.push(`>=${event.introduced}`); } else if (event.fixed) { ranges.push(`<${event.fixed}`); } }); } }); } }); affectedVersions = ranges.join(' || '); } // Extract references const references = (vuln.references || []).map(ref => ref.url); // Create standardized vulnerability object return { id: vuln.id, title: vuln.summary || 'Unknown vulnerability', description: vuln.details || 'No details available', severity, cvss, affectedVersions, safeVersions: null, // Calculate from affected versions if needed published: vuln.published, lastModified: vuln.modified, ecosystem, packageName, version, references, aliases: vuln.aliases || [], source: 'osv' }; }) .filter(Boolean); // Remove any null entries } /** * Find vulnerabilities for a specific package * * @param {string} packageName - Package name * @param {string} version - Package version * @param {string} ecosystem - Package ecosystem (npm, pip, etc.) * @param {Object} options - Additional options * @returns {Promise<Array>} - Array of vulnerabilities */ async findVulnerabilities(packageName, version, ecosystem, options = {}) { try { if (!this.isInitialized) { await this.initialize(); } // Standardize ecosystem name const osvEcosystem = ECOSYSTEM_MAP[ecosystem] || ecosystem; // Check cache first const cacheKey = `${osvEcosystem}:${packageName}:${version}`; // Force refresh or not in cache if (options.refresh || !this.vulnerabilities.has(cacheKey)) { // Query OSV API const results = await this.queryOsvApi(packageName, version, ecosystem); // Process results const vulnerabilities = this.processOsvResults(results, packageName, version, ecosystem); // Update cache this.vulnerabilities.set(cacheKey, vulnerabilities); await this.saveToCache(); return vulnerabilities; } // Return cached results return this.vulnerabilities.get(cacheKey) || []; } catch (error) { console.error(`Error finding OSV vulnerabilities for ${packageName}@${version}`, error); return []; } } }