UNPKG

@proug/mcp-cve-intelligence-server-lite

Version:

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

1,181 lines 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,
        };
    }
    async performSearch(filters, timer) {
        // Build request for available sources
        const result = await this.sourceManager.makeRequestWithFallback('search', (source) => this.buildSearchRequest(source, filters));
        // Process and normalize the results
        const searchResult = this.normalizeSearchResults(result.data, result.source);
        // Ensure searchResult is valid before accessing properties
        if (!searchResult) {
            logger.error('Failed to normalize search results', undefined, {
                source: result.source,
                hasData: !!result.data,
            });
            throw new Error('Failed to process search results');
        }
        const duration = timer.end({
            resultCount: searchResult.totalResults || 0,
            source: result.source,
        });
        logger.cveSearch(filters, searchResult.totalResults || 0, duration);
        return searchResult;
    }
    // Helper methods for getCVEDetails refactoring
    validateCVEId(cveId) {
        if (!isValidCVEId(cveId)) {
            throw secureErrorHandler.createSafeError({
                type: ErrorType.VALIDATION,
                originalError: new Error(`Invalid CVE ID format: ${cveId}`),
                statusCode: 400,
                userMessage: 'Invalid CVE ID format. Expected format: CVE-YYYY-NNNN',
            });
        }
    }
    async fetchCVE(cveId, timer) {
        const enabledSources = this.sourceManager.getEnabledSources().filter(s => s.features.includes('details'));
        if (enabledSources.length === 0) {
            throw new Error('No sources available for CVE details operation');
        }
        let lastError = null;
        // Try each source until we get a valid CVE
        for (const source of enabledSources) {
            try {
                // Get data from this specific source
                const sourceKey = Object.keys(this.sourceManager.getAllSources()).find(key => this.sourceManager.getAllSources()[key].name === source.name);
                if (!sourceKey) {
                    logger.warn(`Could not find source key for: ${source.name}`);
                    continue;
                }
                const { url, options } = this.buildDetailsRequest(source, cveId);
                const result = await this.sourceManager.makeRequest(sourceKey, url, options);
                if (!result?.data) {
                    logger.warn(`No data returned for CVE ${cveId} from source ${source.name}`);
                    continue;
                }
                // Try to normalize the data - this is where errors might occur
                const cve = this.normalizeCVEData(result.data, source.name);
                const duration = timer.end({
                    source: source.name,
                });
                logger.cveRetrieve(cveId, source.name, false, duration);
                return cve;
            }
            catch (error) {
                lastError = error;
                logger.warn(`Failed to get CVE ${cveId} from source ${source.name}`, {
                    error: lastError.message,
                    willTryNext: source !== enabledSources[enabledSources.length - 1],
                });
                continue;
            }
        }
        // If we get here, all sources failed
        timer.end({ error: true });
        throw lastError || new Error(`CVE ${cveId} not found in any available source`);
    }
    /**
     * Calculate risk score based on CVSS score and exploit availability
     */
    calculateRiskScore(cve, hasExploit) {
        const baseScore = this.extractCVSSScore(cve);
        const exploitMultiplier = hasExploit ? 1.5 : 1.0;
        const attackVectorMultiplier = this.extractAttackVector(cve) === 'NETWORK' ? 1.2 : 1.0;
        return Math.min(10.0, baseScore * exploitMultiplier * attackVectorMultiplier);
    }
}
//# sourceMappingURL=cve-service.js.map