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
JavaScript
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,
};
}