UNPKG

mcp-cve-intelligence-server-lite-test

Version:

Lite Model Context Protocol server for comprehensive CVE intelligence gathering with multi-source exploit discovery, designed for security professionals and cybersecurity researchers - Alpha Release

1,098 lines (1,097 loc) 53.9 kB
import { format, subDays } from 'date-fns'; import { SourceFactory } from '../sources/factory.js'; import { createContextLogger, createPerformanceTimer } from '../utils/logger.js'; import { secureErrorHandler, ErrorType } from '../utils/secure-error-handler.js'; import { getAppConfiguration } from '../config/index.js'; import { isValidCVEId, isValidDateRange, parseCPE, normalizeAndDeduplicateCVEIds, } from '../utils/validation.js'; import { getEnglishDescription, normalizeDescriptions } from '../utils/cve-utils.js'; const logger = createContextLogger('CVEService'); export class CVEService { sourceManager; sourceImplementations = {}; constructor(sourceManager) { this.sourceManager = sourceManager; this.initializeSourceImplementations(); } initializeSourceImplementations() { // Get all sources and filter enabled ones as an object const allSources = this.sourceManager.getAllSources(); const enabledSources = Object.fromEntries(Object.entries(allSources).filter(([_, source]) => source.enabled)); // Get API keys from environment const apiKeys = this.getApiKeysFromEnvironment(); // Create source implementations using the factory this.sourceImplementations = SourceFactory.createSourceImplementations(enabledSources, apiKeys); logger.info('Source implementations initialized', { count: Object.keys(this.sourceImplementations).length, sources: Object.keys(this.sourceImplementations), }); } getApiKeysFromEnvironment() { const apiKeys = {}; // Get API keys for each source from the centralized configuration const config = getAppConfiguration(); const allSources = this.sourceManager.getAllSources(); Object.entries(allSources).forEach(([name, source]) => { if (source.apiKeyEnvVar) { const primaryKey = config.security.apiKeys[name.toLowerCase()]; const fallbackKey = config.security.apiKeys[source.apiKeyEnvVar.toLowerCase()]; const apiKey = primaryKey || fallbackKey; if (apiKey) { apiKeys[name] = apiKey; } } }); return apiKeys; } // Main search method - Simple search with client-side sorting async searchCVEs(filters) { const timer = createPerformanceTimer('search_cves'); try { this.validateSearchFilters(filters); // Use simple sorted search for all requests const result = await this.performSortedSearch(filters, timer); // Apply exploit indicator filtering if requested if (filters.hasExploit !== undefined) { const filteredCVEs = result.cves.filter((cve) => { // Use pre-calculated exploit indicators from normalization const hasExploitIndicators = cve.exploitIndicators?.hasExploitIndicators || false; return filters.hasExploit ? hasExploitIndicators : !hasExploitIndicators; }); return { ...result, cves: filteredCVEs, totalResults: filteredCVEs.length, }; } return result; } catch (error) { timer.end({ error: true }); logger.error('CVE search failed', error, { filters }); throw error; } } // Get detailed CVE information async getCVEDetails(cveId) { const timer = createPerformanceTimer('get_cve_details'); try { this.validateCVEId(cveId); const result = await this.fetchCVE(cveId, timer); return result; } catch (error) { timer.end({ error: true }); logger.error('Failed to get CVE details', error, { cveId }); throw error; } } // Get trending CVEs using intelligent sorting async getTrendingCVEs(limit = 20) { const timer = createPerformanceTimer('get_trending_cves'); try { // For trending, we want to get a broader dataset to properly identify trends // Use intelligent sorting to get high-severity CVEs from recent months const startDate = `${format(subDays(new Date(), 90), 'yyyy-MM-dd')}T00:00:00.000Z`; // Start of day 90 days ago const endDate = `${format(new Date(), 'yyyy-MM-dd')}T23:59:59.999Z`; // Today end of day const baseFilters = { pubStartDate: startDate, pubEndDate: endDate, resultsPerPage: limit * 3, // Fetch 3x per severity to ensure good data sortBy: 'published', // Sort by publication date for trending analysis (newest first) }; // Fetch HIGH and CRITICAL severity CVEs separately const [highResults, criticalResults] = await Promise.all([ this.performSortedSearch({ ...baseFilters, cvssV3Severity: 'HIGH' }, timer), this.performSortedSearch({ ...baseFilters, cvssV3Severity: 'CRITICAL' }, timer), ]); // Combine and deduplicate results const allCVEs = [...highResults.cves, ...criticalResults.cves]; const uniqueCVEs = allCVEs.filter((cve, index, array) => array.findIndex(c => c.id === cve.id) === index); // Calculate trending scores and sort by score (highest first) const trendingCVEs = await this.calculateTrendingScores(uniqueCVEs); // Return top results sorted by trending score const topTrending = trendingCVEs .sort((a, b) => b.trendingScore - a.trendingScore) .slice(0, limit); timer.end({ resultCount: topTrending.length }); return topTrending; } catch (error) { timer.end({ error: true }); logger.error('Failed to get trending CVEs', error); throw error; } } // Calculate EPSS scores for vulnerability prioritization async calculateEPSSScores(request) { const timer = createPerformanceTimer('calculate_epss_scores'); try { // Normalize and deduplicate CVE IDs first const uniqueCveIds = normalizeAndDeduplicateCVEIds(request.cveIds); if (uniqueCveIds.length === 0) { throw secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('No valid CVE IDs provided for EPSS analysis'), statusCode: 400, userMessage: 'No valid CVE IDs provided. Please check the CVE ID format (CVE-YYYY-NNNN).', }); } const cves = []; const epssScores = []; // Get details for all unique CVEs for (const cveId of uniqueCveIds) { const cve = await this.getCVEDetails(cveId); cves.push(cve); // Calculate EPSS score for this CVE const epssScore = await this.calculateIndividualEPSSScore(cve, request.environmentContext); epssScores.push(epssScore); } // Sort by EPSS score (highest first) epssScores.sort((a, b) => b.epssScore - a.epssScore); // Generate summary and recommendations const result = this.generateEPSSResult(epssScores, request); timer.end({ cveCount: uniqueCveIds.length, originalCount: request.cveIds.length }); return result; } catch (error) { timer.end({ error: true }); logger.error('Failed to calculate EPSS scores', error, { cveIds: request.cveIds }); throw error; } } async calculateIndividualEPSSScore(cve, environmentContext) { // Get EPSS configuration from source manager or use defaults const epssConfig = this.getEPSSConfig(); // Extract CVE data const cvssScore = this.extractCVSSScore(cve); const hasExploitIndicators = cve.exploitIndicators?.hasExploitIndicators || false; const ageInDays = this.calculateAgeInDays(cve.published); const attackVector = this.extractAttackVector(cve); const attackComplexity = this.extractAttackComplexity(cve); const privilegesRequired = this.extractPrivilegesRequired(cve); const userInteraction = this.extractUserInteraction(cve); // Calculate base score (CVSS component) const baseScore = (cvssScore / 10) * epssConfig.baseFactors.cvssWeight; // Calculate exploitability score const exploitabilityScore = this.calculateExploitabilityScore(attackVector, attackComplexity, privilegesRequired, userInteraction, epssConfig); // Calculate contextual score (exploit maturity, age, etc.) const contextualScore = this.calculateContextualScore(cve, hasExploitIndicators, epssConfig); // Calculate age score const ageScore = this.calculateAgeScore(ageInDays, epssConfig.baseFactors.ageWeight); // Combine all scores let epssScore = baseScore + exploitabilityScore + contextualScore + ageScore; // Apply environmental adjustments if provided let environmentalAdjustment = 1.0; if (environmentContext) { environmentalAdjustment = this.calculateEnvironmentalAdjustment(environmentContext); epssScore *= environmentalAdjustment; } // Use a wider scale (0-1.2) before normalization to allow better differentiation // This allows environmental factors to push scores above 1.0 initially const maxPossibleScore = 1.2; // Normalize to 0-1 scale using sigmoid-like function for better distribution if (epssScore <= 1.0) { // Keep scores <= 1.0 mostly unchanged epssScore = Math.min(1.0, Math.max(0.0, epssScore)); } else { // Compress scores > 1.0 into the 0.85-1.0 range for better differentiation const excess = epssScore - 1.0; const compressedExcess = (excess / (maxPossibleScore - 1.0)) * 0.15; epssScore = Math.min(1.0, 0.85 + compressedExcess); } // Determine risk level const riskLevel = this.determineRiskLevel(epssScore); // Generate recommendations const recommendations = this.generateEPSSRecommendations(cve, epssScore, hasExploitIndicators, environmentContext); return { cveId: cve.id, epssScore, riskLevel, cvssScore, baseScore, exploitabilityScore, contextualScore, ageScore, exploitMaturity: this.assessExploitMaturity(cve, hasExploitIndicators), factors: { attackVector, attackComplexity, privilegesRequired, userInteraction, hasExploitIndicators, }, environmentalAdjustment: environmentalAdjustment !== 1.0 ? environmentalAdjustment : undefined, recommendations, }; } getEPSSConfig() { // Access EPSS config from the source manager const epssConfig = this.sourceManager.getEPSSConfig(); if (epssConfig) { return epssConfig; } // Fallback to default EPSS configuration logger.warn('Using default EPSS configuration - no configuration found in sources'); return { baseFactors: { cvssWeight: 0.3, exploitAvailabilityWeight: 0.25, ageWeight: 0.15, }, exploitabilityFactors: { attackVectorWeight: 0.1, attackComplexityWeight: 0.08, privilegesRequiredWeight: 0.07, userInteractionWeight: 0.05, }, contextualFactors: { exploitMaturityWeight: 0.1, publicDisclosureWeight: 0.05, }, factors: { attackVector: { 'NETWORK': 1.0, 'ADJACENT': 0.7, 'LOCAL': 0.4, 'PHYSICAL': 0.1 }, attackComplexity: { 'LOW': 1.0, 'HIGH': 0.3 }, privilegesRequired: { 'NONE': 1.0, 'LOW': 0.6, 'HIGH': 0.3 }, userInteraction: { 'NONE': 1.0, 'REQUIRED': 0.5 }, exploitMaturity: { 'UNPROVEN': 0.1, 'PROOF_OF_CONCEPT': 0.4, 'FUNCTIONAL': 0.7, 'HIGH': 1.0 }, }, }; } calculateExploitabilityScore(attackVector, attackComplexity, privilegesRequired, userInteraction, config) { const vectorScore = config.factors.attackVector[attackVector] || 0; const complexityScore = config.factors.attackComplexity[attackComplexity] || 0; const privilegeScore = config.factors.privilegesRequired[privilegesRequired] || 0; const interactionScore = config.factors.userInteraction[userInteraction] || 0; return (vectorScore * config.exploitabilityFactors.attackVectorWeight + complexityScore * config.exploitabilityFactors.attackComplexityWeight + privilegeScore * config.exploitabilityFactors.privilegesRequiredWeight + interactionScore * config.exploitabilityFactors.userInteractionWeight); } calculateContextualScore(cve, hasExploitIndicators, config) { let score = 0; // Exploit availability if (hasExploitIndicators) { score += config.baseFactors.exploitAvailabilityWeight; } // Exploit maturity assessment const exploitMaturity = this.assessExploitMaturity(cve, hasExploitIndicators); const maturityScore = config.factors.exploitMaturity[exploitMaturity] || 0; score += maturityScore * config.contextualFactors.exploitMaturityWeight; return score; } assessExploitMaturity(cve, hasExploit) { if (!hasExploit) { return 'UNPROVEN'; } // Use pre-calculated exploit indicators directly from CVE normalization const indicators = cve.exploitIndicators?.indicators || []; if (indicators.length === 0) { // Fallback: if hasExploit is true but no detailed indicators, assume basic PoC return 'PROOF_OF_CONCEPT'; } // Count different types of exploit sources for maturity assessment let highMaturityCount = 0; let functionalCount = 0; let pocCount = 0; for (const indicator of indicators) { const source = indicator.source.toLowerCase(); const title = indicator.title.toLowerCase(); const type = indicator.type.toLowerCase(); const isVerified = indicator.verified === true; // High maturity: Professional frameworks and verified exploits if (source.includes('metasploit') || title.includes('metasploit') || type.includes('metasploit') || (isVerified && source.includes('exploit-db'))) { highMaturityCount++; } // Functional: Automated tools, scanners, and multiple PoCs else if (source.includes('nuclei') || source.includes('nmap') || title.includes('scanner') || type.includes('scanner') || source.includes('exploit-db')) { functionalCount++; } // PoC level: GitHub repositories, research, etc. else { pocCount++; } } // Determine maturity based on quality and quantity if (highMaturityCount > 0) { return 'HIGH'; } if (functionalCount >= 2 || (functionalCount >= 1 && pocCount >= 1)) { return 'FUNCTIONAL'; } return 'PROOF_OF_CONCEPT'; } calculateAgeScore(ageInDays, ageWeight) { // Newer vulnerabilities have higher exploitation probability // Use a more gradual decay: full score for <30 days, half score at 365 days, minimal at 3+ years let ageFactor; if (ageInDays <= 30) { // Full weight for very recent CVEs (last 30 days) ageFactor = 1.0; } else if (ageInDays <= 365) { // Linear decay from 1.0 to 0.5 over the first year ageFactor = 1.0 - (ageInDays - 30) * 0.5 / 335; } else if (ageInDays <= 1095) { // Slower decay from 0.5 to 0.1 over years 2-3 ageFactor = 0.5 - (ageInDays - 365) * 0.4 / 730; } else { // Minimal but non-zero score for very old CVEs ageFactor = 0.1; } return ageFactor * ageWeight; } calculateEnvironmentalAdjustment(context) { let adjustment = 1.0; // Network exposure multiplier if (context.networkExposure) { const exposureMultipliers = { 'internet-facing': 1.5, 'internal': 1.0, 'air-gapped': 0.3, }; adjustment *= exposureMultipliers[context.networkExposure]; } // Asset criticality multiplier if (context.assetCriticality) { const criticalityMultipliers = { 'critical': 1.4, 'high': 1.2, 'medium': 1.0, 'low': 0.8, }; adjustment *= criticalityMultipliers[context.assetCriticality]; } // Security controls reduction if (context.securityControls) { const controlReductions = { 'waf': 0.8, 'ids': 0.9, 'antivirus': 0.85, 'dlp': 0.95, 'segmented': 0.7, }; for (const control of context.securityControls) { adjustment *= controlReductions[control]; } } return adjustment; } determineRiskLevel(epssScore) { // Adjusted thresholds to work better with improved scoring if (epssScore >= 0.85) { return 'CRITICAL'; } if (epssScore >= 0.65) { return 'HIGH'; } if (epssScore >= 0.35) { return 'MEDIUM'; } return 'LOW'; } generateEPSSRecommendations(cve, epssScore, hasExploitIndicators, environmentContext) { const recommendations = []; // Risk-based recommendations if (epssScore >= 0.8) { recommendations.push('CRITICAL: Immediate patching required within 24 hours'); recommendations.push('Consider emergency change management procedures'); } else if (epssScore >= 0.6) { recommendations.push('HIGH: Patch within 7 days'); recommendations.push('Implement monitoring for exploitation attempts'); } else if (epssScore >= 0.3) { recommendations.push('MEDIUM: Patch within 30 days'); } else { recommendations.push('LOW: Patch during next maintenance window'); } // Exploit-specific recommendations if (hasExploitIndicators) { recommendations.push('Public exploits available - monitor for active exploitation'); recommendations.push('Consider temporary mitigations if patching is delayed'); } // Network-specific recommendations const attackVector = this.extractAttackVector(cve); if (attackVector === 'NETWORK') { recommendations.push('Network-accessible vulnerability - review firewall rules'); if (environmentContext?.networkExposure === 'internet-facing') { recommendations.push('Internet-facing systems at highest risk'); } } // Environment-specific recommendations if (environmentContext?.assetCriticality === 'critical') { recommendations.push('Critical asset affected - prioritize patching'); } return recommendations; } generateEPSSResult(scores, request) { const totalCVEs = scores.length; const averageEPSSScore = scores.reduce((sum, score) => sum + score.epssScore, 0) / totalCVEs; const highRiskCount = scores.filter(s => s.riskLevel === 'HIGH' || s.riskLevel === 'CRITICAL').length; const criticalRiskCount = scores.filter(s => s.riskLevel === 'CRITICAL').length; // Generate overall recommendations const recommendations = []; if (criticalRiskCount > 0) { recommendations.push(`URGENT: ${criticalRiskCount} CVE(s) require immediate attention`); } if (highRiskCount > criticalRiskCount) { recommendations.push(`${highRiskCount - criticalRiskCount} CVE(s) require patching within 7 days`); } const hasNetworkVulns = scores.some(s => s.factors.attackVector === 'NETWORK'); if (hasNetworkVulns && request.environmentContext?.networkExposure === 'internet-facing') { recommendations.push('Network vulnerabilities on internet-facing systems pose elevated risk'); } return { summary: { totalCVEs, averageEPSSScore: Math.round(averageEPSSScore * 100) / 100, highRiskCount, criticalRiskCount, environmentContext: request.environmentContext, }, scores, priorityList: scores.map(s => s.cveId), recommendations, generatedAt: new Date().toISOString(), }; } // Generate CVE report async generateReport(reportConfig) { const timer = createPerformanceTimer('generate_report'); try { // Normalize and deduplicate CVE IDs first const uniqueCveIds = normalizeAndDeduplicateCVEIds(reportConfig.cveIds); if (uniqueCveIds.length === 0) { throw new Error('No valid CVE IDs provided for report generation'); } const cves = []; const exploitIndicators = {}; const insights = {}; // Gather all data for unique CVEs for (const cveId of uniqueCveIds) { const cve = await this.getCVEDetails(cveId); cves.push(cve); if (reportConfig.includeExploits) { // Use pre-calculated exploit indicators from normalization exploitIndicators[cveId] = cve.exploitIndicators?.hasExploitIndicators || false; } insights[cveId] = this.generatePentestingInsights(cve, exploitIndicators[cveId]); } // Generate report based on format let report; switch (reportConfig.format) { case 'markdown': report = this.generateMarkdownReport(cves, exploitIndicators, insights, reportConfig); break; case 'json': { // Transform CVEs to use description strings instead of descriptions arrays const { deepTransformCVEsForDisplay } = await import('../utils/cve-utils.js'); const reportData = { cves, exploitIndicators, insights }; const transformedData = deepTransformCVEsForDisplay(reportData); report = JSON.stringify(transformedData, null, 2); break; } case 'summary': report = this.generateSummaryReport(cves, exploitIndicators, insights, reportConfig); break; default: throw new Error(`Unsupported report format: ${reportConfig.format}`); } timer.end({ format: reportConfig.format, cveCount: uniqueCveIds.length, originalCount: reportConfig.cveIds.length, }); return report; } catch (error) { timer.end({ error: true }); logger.error('Failed to generate report', error, { reportConfig }); throw error; } } // Search by CPE async searchByCPE(cpe, filters) { const timer = createPerformanceTimer('search_by_cpe'); try { // Parse and validate CPE const parsedCPE = parseCPE(cpe); if (!parsedCPE.vendor && !parsedCPE.product) { throw secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('CPE must contain at least vendor or product information'), statusCode: 400, userMessage: 'CPE string must contain at least vendor or product information.', }); } // Build search filters const searchFilters = { cpeNameId: cpe, ...filters, }; const result = await this.searchCVEs(searchFilters); timer.end({ cpe, resultCount: result.totalResults }); return result; } catch (error) { timer.end({ error: true }); logger.error('Failed to search by CPE', error, { cpe }); throw error; } } // Private helper methods validateSearchFilters(filters) { if (filters.pubStartDate && filters.pubEndDate) { if (!isValidDateRange(filters.pubStartDate, filters.pubEndDate)) { throw secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Invalid date range: start date must be before end date'), statusCode: 400, }); } } if (filters.lastModStartDate && filters.lastModEndDate) { if (!isValidDateRange(filters.lastModStartDate, filters.lastModEndDate)) { throw secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Invalid last modified date range'), statusCode: 400, }); } } if (filters.resultsPerPage && (filters.resultsPerPage < 1 || filters.resultsPerPage > 2000)) { throw secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Results per page must be between 1 and 2000'), statusCode: 400, }); } // Info about comprehensive data fetching for direct search if (filters.resultsPerPage && filters.resultsPerPage > 50) { logger.info('Large result set requested for direct search - will fetch all available data and sort', { requestedLimit: filters.resultsPerPage, sortBy: filters.sortBy || 'published', note: 'Direct search fetches complete dataset for accurate top-N results', }); } } buildSearchRequest(source, filters) { // Find the source implementation const sourceImpl = this.getSourceImplementation(source); if (sourceImpl) { return sourceImpl.buildSearchRequest(filters); } // Fallback to basic URL construction logger.warn(`No implementation found for source: ${source.name}, using fallback`); const url = `${source.baseUrl}`; return { url, options: {} }; } buildDetailsRequest(source, cveId) { // Find the source implementation const sourceImpl = this.getSourceImplementation(source); if (sourceImpl) { return sourceImpl.buildDetailsRequest(cveId); } // Fallback to basic URL construction logger.warn(`No implementation found for source: ${source.name}, using fallback`); return { url: `${source.baseUrl}/${cveId}`, options: {} }; } getSourceImplementation(source) { // Find implementation by matching source config return Object.values(this.sourceImplementations).find(impl => impl.getName() === source.name && impl.getBaseUrl() === source.baseUrl); } getSourceImplementationByName(sourceName) { // Find implementation by source name return Object.values(this.sourceImplementations).find(impl => impl.getName() === sourceName); } normalizeSearchResults(data, sourceName) { // Find the source implementation by name const sourceImpl = this.getSourceImplementationByName(sourceName); if (sourceImpl) { return sourceImpl.normalizeSearchResults(data); } // Fallback normalization for unknown sources logger.warn(`No implementation found for source: ${sourceName}, using fallback normalization`); // Default normalization for other sources if (Array.isArray(data)) { const genericData = data; return { cves: genericData.map((cve) => this.normalizeCVEData(cve, sourceName)), totalResults: genericData.length, startIndex: 0, resultsPerPage: genericData.length, format: 'Generic', version: '1.0', timestamp: new Date().toISOString(), }; } // Handle object-style responses (e.g., {cves: [...], totalResults: 5}) if (data && typeof data === 'object') { const objData = data; // Look for common CVE array property names const cveArray = objData.cves || objData.results || objData.data || objData.vulnerabilities; if (Array.isArray(cveArray)) { return { cves: cveArray.map((cve) => this.normalizeCVEData(cve, sourceName)), totalResults: objData.totalResults || cveArray.length, startIndex: objData.startIndex || 0, resultsPerPage: objData.resultsPerPage || cveArray.length, format: 'Generic', version: '1.0', timestamp: new Date().toISOString(), }; } } // Fallback for empty or invalid data return { cves: [], totalResults: 0, startIndex: 0, resultsPerPage: 0, format: 'Generic', version: '1.0', timestamp: new Date().toISOString(), }; } normalizeCVEData(data, sourceName) { // Find the source implementation by name first - let it handle its own data format const sourceImpl = this.getSourceImplementationByName(sourceName); if (sourceImpl) { return sourceImpl.normalizeCVEData(data); } // Fallback normalization for unknown sources only logger.warn(`No implementation found for source: ${sourceName}, using fallback CVE normalization`); // Validate essential CVE data structure for fallback normalization const cveId = data.id || data.cveId || data.cve_id; if (!cveId || cveId === 'unknown') { // Check if this is an empty response (common for non-existent CVEs) if (data && typeof data === 'object' && Object.keys(data).length === 0) { throw new Error('CVE not found: empty response from source'); } // Generic missing ID error throw new Error('Invalid CVE data: missing or invalid CVE ID'); } // This is a simplified normalization - in practice, you'd need // comprehensive mapping for each source's data format return { id: cveId, sourceIdentifier: data.sourceIdentifier || sourceName, published: data.published || data.publishedDate || data.published_date || new Date().toISOString(), lastModified: data.lastModified || data.last_modified || new Date().toISOString(), vulnStatus: (data.vulnStatus || data.status || data.state || 'PUBLISHED'), descriptions: data.descriptions ? normalizeDescriptions(data.descriptions) : normalizeDescriptions(data.description), metrics: undefined, // Will be normalized by source-specific implementations weaknesses: undefined, // Will be normalized by source-specific implementations configurations: undefined, // Will be normalized by source-specific implementations references: undefined, // Will be normalized by source-specific implementations }; } /** * Get exploit indicators for a CVE (uses pre-calculated values from normalization) */ getExploitIndicators(cve) { return { hasExploitIndicators: cve.exploitIndicators?.hasExploitIndicators || false, indicators: cve.exploitIndicators?.indicators || [], calculatedAt: cve.exploitIndicators?.calculatedAt, }; } async calculateTrendingScores(cves) { const trending = []; // Get scoring configuration with defaults const scoringConfig = this.sourceManager.getScoringConfig(); const config = scoringConfig || { exploitAvailabilityWeight: 0.4, cvssWeight: 0.3, ageWeight: 0.3, }; for (const cve of cves) { const baseScore = this.extractCVSSScore(cve); const severity = this.extractSeverity(cve); // Get CVE data for scoring const hasExploitIndicators = cve.exploitIndicators?.hasExploitIndicators || false; const ageInDays = this.calculateAgeInDays(cve.published); const attackVector = this.extractAttackVector(cve); // Try to match against tiers first let trendingScore = this.evaluateTiers(baseScore, hasExploitIndicators, ageInDays, attackVector, config); // If no tier matched, use fallback weighted scoring if (trendingScore === null) { trendingScore = this.calculateWeightedScore(baseScore, hasExploitIndicators, ageInDays, config); } trending.push({ cveId: cve.id, title: getEnglishDescription(cve.descriptions) ? `${getEnglishDescription(cve.descriptions).substring(0, 100)}...` : 'No description', severity, baseScore, trendingScore: Math.round(trendingScore * 100) / 100, // Round to 2 decimal places affectedProducts: this.extractAffectedProducts(cve), source: cve.sourceIdentifier || 'Unknown', publishedDate: cve.published, }); } return trending; } evaluateTiers(cvssScore, hasExploit, ageInDays, attackVector, config) { const tiers = config.tiers || []; for (const tier of tiers) { for (const condition of tier.conditions) { if (this.matchesCondition(cvssScore, hasExploit, ageInDays, attackVector, condition)) { return tier.score * 100; // Convert to 0-100 scale to match legacy scoring } } } return null; // No tier matched } matchesCondition(cvssScore, hasExploit, ageInDays, attackVector, condition) { // Check CVSS score condition if (condition.cvssScore !== undefined) { const op = condition.operator || '>='; if (op === '>=' && !(cvssScore >= condition.cvssScore)) { return false; } if (op === '>' && !(cvssScore > condition.cvssScore)) { return false; } if (op === '<=' && !(cvssScore <= condition.cvssScore)) { return false; } if (op === '<' && !(cvssScore < condition.cvssScore)) { return false; } } // Check exploit condition if (condition.hasExploit !== undefined && condition.hasExploit !== hasExploit) { return false; } // Check age condition if (condition.ageInDays !== undefined) { const op = condition.ageOperator || '<'; if (op === '<' && !(ageInDays < condition.ageInDays)) { return false; } if (op === '<=' && !(ageInDays <= condition.ageInDays)) { return false; } if (op === '>=' && !(ageInDays >= condition.ageInDays)) { return false; } if (op === '>' && !(ageInDays > condition.ageInDays)) { return false; } } // Check attack vector conditions (e.g., network vector) if (condition.vector && attackVector) { if (!attackVector.toLowerCase().includes(condition.vector.toLowerCase())) { return false; } } return true; // All conditions passed } calculateWeightedScore(cvssScore, hasExploit, ageInDays, config) { // Support both new fallback config and legacy config const weights = config.fallback || { exploitAvailabilityWeight: config.exploitAvailabilityWeight || 0.4, cvssWeight: config.cvssWeight || 0.3, ageWeight: config.ageWeight || 0.3, }; // CVSS component (0-10 normalized to 0-100) const cvssComponent = (cvssScore / 10) * 100 * weights.cvssWeight; // Exploit availability component (0-100) const exploitComponent = hasExploit ? 100 * weights.exploitAvailabilityWeight : 0; // Age component (newer CVEs get higher scores, max 100) // Use logarithmic decay to avoid over-penalizing slightly older CVEs const ageScore = Math.max(0, 1 - (Math.log(ageInDays + 1) / Math.log(90 + 1))); const ageComponent = ageScore * 100 * weights.ageWeight; return cvssComponent + exploitComponent + ageComponent; } calculateAgeInDays(publishedDate) { if (!publishedDate) { return 365; } // Default to 1 year old if no date const published = new Date(publishedDate); const now = new Date(); // Calculate difference in days const diffTime = now.getTime() - published.getTime(); return Math.floor(diffTime / (1000 * 60 * 60 * 24)); } generatePentestingInsights(cve, hasExploitIndicators) { const baseScore = this.extractCVSSScore(cve); const attackVector = this.extractAttackVector(cve); const complexity = this.extractAttackComplexity(cve); const privilegesRequired = this.extractPrivilegesRequired(cve); const userInteraction = this.extractUserInteraction(cve); let riskScore = baseScore; if (hasExploitIndicators) { riskScore += 2; } if (attackVector === 'NETWORK') { riskScore += 1; } if (complexity === 'LOW') { riskScore += 1; } const recommendedActions = this.generateRecommendations(cve, hasExploitIndicators); const pentestingNotes = this.generatePentestingNotes(cve, hasExploitIndicators); return { cveId: cve.id, riskScore: Math.min(riskScore, 10), attackVector, privilegesRequired, userInteraction, recommendedActions, pentestingNotes, }; } generateMarkdownReport(cves, exploitIndicators, insights, config) { const report = [ '# CVE Intelligence Report', `Generated: ${new Date().toISOString()}`, `CVEs Analyzed: ${cves.length}`, '', '## Executive Summary', '', ]; const highRisk = Object.values(insights).filter(i => i.riskScore >= 7).length; const withExploits = Object.values(exploitIndicators).filter(hasExploit => hasExploit).length; report.push(`- **High Risk CVEs**: ${highRisk}`); report.push(`- **CVEs with Available Exploits**: ${withExploits}`); report.push(''); // Detailed analysis for each CVE report.push('## Detailed Analysis'); report.push(''); for (const cve of cves) { const insight = insights[cve.id]; const hasExploitIndicators = exploitIndicators[cve.id]; report.push(`### ${cve.id}`); report.push(`**Risk Score**: ${insight.riskScore}/10`); report.push(`**CVSS Score**: ${this.extractCVSSScore(cve)}`); report.push(`**Severity**: ${this.extractSeverity(cve)}`); report.push(''); report.push('**Description**:'); report.push(getEnglishDescription(cve.descriptions)); report.push(''); if (config.includeExploits) { report.push('**Exploit Status**:'); if (hasExploitIndicators) { report.push('- Exploit indicators found in references'); } else { report.push('- No exploit indicators found'); } report.push(''); } if (config.includeRecommendations) { report.push('**Recommendations**:'); insight.recommendedActions.forEach(action => { report.push(`- ${action}`); }); report.push(''); } report.push('---'); report.push(''); } return report.join('\n'); } generateSummaryReport(cves, exploitIndicators, insights, _config) { const highRisk = Object.values(insights).filter(i => i.riskScore >= 7); const withExploits = Object.entries(exploitIndicators) .filter(([_, hasExploit]) => hasExploit) .map(([cveId, _]) => ({ cveId })); const report = [ 'CVE Intelligence Summary', '=========================', `Total CVEs: ${cves.length}`, `High Risk (7+): ${highRisk.length}`, `With Exploit Indicators: ${withExploits.length}`, '', 'Priority CVEs:', ...highRisk.slice(0, 5).map(i => `- ${i.cveId} (Risk: ${i.riskScore})`), '', 'CVEs with Exploit Indicators:', ...withExploits.slice(0, 3).map(e => `- ${e.cveId} (exploit indicators found)`), ]; return report.join('\n'); } /** * Sort CVEs based on specified criteria (always descending order) */ sortCVEs(cves, sortBy) { cves.sort((a, b) => { let compareResult = 0; switch (sortBy) { case 'published': { const dateA = new Date(a.published || '1970-01-01'); const dateB = new Date(b.published || '1970-01-01'); compareResult = dateA.getTime() - dateB.getTime(); } break; case 'lastModified': { const dateA = new Date(a.lastModified || '1970-01-01'); const dateB = new Date(b.lastModified || '1970-01-01'); compareResult = dateA.getTime() - dateB.getTime(); } break; case 'cvssScore': { const scoreA = this.extractCVSSScore(a); const scoreB = this.extractCVSSScore(b); compareResult = scoreA - scoreB; } break; case 'severity': { const severityOrder = { 'CRITICAL': 4, 'HIGH': 3, 'MEDIUM': 2, 'LOW': 1, 'NONE': 0 }; const severityA = severityOrder[this.extractSeverity(a)] || 0; const severityB = severityOrder[this.extractSeverity(b)] || 0; compareResult = severityA - severityB; } break; default: // Default to published date { const dateA = new Date(a.published || '1970-01-01'); const dateB = new Date(b.published || '1970-01-01'); compareResult = dateA.getTime() - dateB.getTime(); } } // Always return descending order (newest/highest first) return -compareResult; }); } // Helper methods for extracting CVSS data extractCVSSScore(cve) { // Prioritize CVSS v4 > v3.1 > v3.0 > v2.0 const cvss4 = cve.metrics?.cvssMetricV4?.[0]?.cvssData?.baseScore; const cvss31 = cve.metrics?.cvssMetricV31?.[0]?.cvssData?.baseScore; const cvss30 = cve.metrics?.cvssMetricV30?.[0]?.cvssData?.baseScore; const cvss2 = cve.metrics?.cvssMetricV2?.[0]?.cvssData?.baseScore; const score = cvss4 ?? cvss31 ?? cvss30 ?? cvss2 ?? 0; // Normalize negative scores to 0 (edge case handling) return Math.max(0, score); } extractSeverity(cve) { const score = this.extractCVSSScore(cve); if (score >= 9.0) { return 'CRITICAL'; } if (score >= 7.0) { return 'HIGH'; } if (score >= 4.0) { return 'MEDIUM'; } if (score > 0) { return 'LOW'; } return 'NONE'; } extractAttackVector(cve) { // Prioritize CVSS v4 > v3.1 > v3.0 return cve.metrics?.cvssMetricV4?.[0]?.cvssData?.attackVector ?? cve.metrics?.cvssMetricV31?.[0]?.cvssData?.attackVector ?? cve.metrics?.cvssMetricV30?.[0]?.cvssData?.attackVector ?? 'UNKNOWN'; } extractAttackComplexity(cve) { // Prioritize CVSS v4 > v3.1 > v3.0 return cve.metrics?.cvssMetricV4?.[0]?.cvssData?.attackComplexity ?? cve.metrics?.cvssMetricV31?.[0]?.cvssData?.attackComplexity ?? cve.metrics?.cvssMetricV30?.[0]?.cvssData?.attackComplexity ?? 'UNKNOWN'; } extractPrivilegesRequired(cve) { // Prioritize CVSS v4 > v3.1 > v3.0 return cve.metrics?.cvssMetricV4?.[0]?.cvssData?.privilegesRequired ?? cve.metrics?.cvssMetricV31?.[0]?.cvssData?.privilegesRequired ?? cve.metrics?.cvssMetricV30?.[0]?.cvssData?.privilegesRequired ?? 'UNKNOWN'; } extractUserInteraction(cve) { // Prioritize CVSS v4 > v3.1 > v3.0 return cve.metrics?.cvssMetricV4?.[0]?.cvssData?.userInteraction ?? cve.metrics?.cvssMetricV31?.[0]?.cvssData?.userInteraction ?? cve.metrics?.cvssMetricV30?.[0]?.cvssData?.userInteraction ?? 'UNKNOWN'; } extractAffectedProducts(cve) { const products = []; if (cve.configurations) { for (const config of cve.configurations) { for (const node of config.nodes) { for (const cpeMatch of node.cpeMatch) { const parsed = parseCPE(cpeMatch.criteria); if (parsed.product) { const product = `${parsed.vendor || 'unknown'}:${parsed.product}`; if (!products.includes(product)) { products.push(product); } } } } } } return products; } generateRecommendations(cve, hasExploitIndicators) { const recommendations = []; const severity = this.extractSeverity(cve); if (severity === 'CRITICAL' || severity === 'HIGH') { recommendations.push('Immediate patching required'); } if (hasExploitIndicators) { recommendations.push('Exploit indicators found in references - review and prioritize'); recommendations.push('Monitor for signs of exploitation'); } if (this.extractAttackVector(cve) === 'NETWORK') { recommendations.push('Consider network segmentation'); recommendations.push('Review firewall rules'); } recommendations.push('Update to latest vendor patches'); recommendations.push('Implement defense-in-depth measures'); return recommendations; } generatePentestingNotes(cve, hasExploitIndicators) { const notes = []; if (hasExploitIndicators) { notes.push('Exploit indicators found in references - investigate for potential exploitation techniques'); } const complexity = this.extractAttackComplexity(cve); if (complexity === 'LOW') { notes.push('Low complexity - good candidate for automated exploitation'); } const userInteraction = this.extractUserInteraction(cve); if (userInteraction === 'REQUIRED') { notes.push('Requires user interaction - consider social engineering vectors'); } return notes; } async performSortedSearch(filters, timer) { const userRequestedLimit = filters.resultsPerPage || 20; const fetchLimit = Math.max(userRequestedLimit * 2, 50); // Fetch 2x for better sorting const result = await this.performSearch({ ...filters, resultsPerPage: fetchLimit }, timer); this.sortCVEs(result.cves, filters.sortBy || 'published'); // Apply user limit after sorting const finalCVEs = result.cves.slice(0, userRequestedLimit); return { ...result, cves: finalCVEs, resultsPerPage: userRequestedLimit, }; }