UNPKG

mcp-cve-intelligence-server-lite-test

Version:

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

296 lines 12.6 kB
import { getAppConfiguration, DEFAULT_HEADERS } from '../config/index.js'; import { createContextLogger } from '../utils/logger.js'; import { SourceFactory } from '../sources/factory.js'; const logger = createContextLogger('SourceManager'); export class SourceManager { sources; appConfig; constructor() { this.appConfig = getAppConfiguration(); // Convert centralized source configs to CVESource format this.sources = {}; Object.entries(this.appConfig.sources).forEach(([key, sourceConfig]) => { this.sources[key] = { name: sourceConfig.name, baseUrl: sourceConfig.baseUrl, enabled: sourceConfig.enabled, priority: sourceConfig.priority, features: sourceConfig.features, endpoints: sourceConfig.endpoints, apiKeyRequired: sourceConfig.apiKeyRequired, apiKeyEnvVar: sourceConfig.apiKeyEnvVar, authHeaderType: sourceConfig.authHeaderType, authHeaderName: sourceConfig.authHeaderName, requestTimeout: sourceConfig.requestTimeout, retryAttempts: sourceConfig.retryAttempts, retryDelayMs: sourceConfig.retryDelay, }; }); logger.info('Source manager initialized', { enabledSources: Object.keys(this.sources).filter(key => this.sources[key].enabled), }); } // Get enabled sources sorted by priority getEnabledSources() { return Object.values(this.sources) .filter(source => source.enabled) .sort((a, b) => a.priority - b.priority); } // Get all sources as object getAllSources() { return this.sources; } // Get specific source by name getSource(name) { return this.sources[name]; } // Get scoring configuration getScoringConfig() { // Return a default scoring config that matches the ScoringConfig interface return { exploitAvailabilityWeight: 2.0, cvssWeight: 1.0, ageWeight: 0.1, }; } // Get EPSS configuration getEPSSConfig() { // Access EPSS configuration from the centralized app config return this.appConfig.epss; } // Check if we can make a request to a source async canMakeRequest(sourceName) { const source = this.sources[sourceName]; if (!source) { return false; } return source.enabled; } // Make an API request with retry logic async makeRequest(sourceName, url, options = {}) { const source = this.sources[sourceName]; if (!source) { throw new Error(`Unknown source: ${sourceName}`); } // Check if source is enabled if (!source.enabled) { throw new Error(`Source ${sourceName} is disabled`); } // Get retry configuration from source or defaults const retryAttempts = source.retryAttempts ?? 3; const retryDelayMs = source.retryDelayMs ?? 1000; const retryBackoffMultiplier = source.retryBackoffMultiplier ?? 2; const retryOnStatusCodes = source.retryOnStatusCodes ?? [429, 502, 503, 504]; let lastError = null; // Try the request with retries for (let attempt = 0; attempt <= retryAttempts; attempt++) { const isRetry = attempt > 0; const startTime = Date.now(); try { // Add delay for retries (exponential backoff) if (isRetry) { const delay = retryDelayMs * Math.pow(retryBackoffMultiplier, attempt - 1); const retryMsg = `Retrying request for ${sourceName} ` + `(attempt ${attempt}/${retryAttempts}) after ${delay}ms delay`; logger.info(retryMsg, { url, attempt, delay, }); await new Promise(resolve => setTimeout(resolve, delay)); } // Prepare headers const headers = { ...DEFAULT_HEADERS, ...options.headers, }; // Use source implementation's authentication headers try { const sourceImpl = SourceFactory.createSourceImplementation(sourceName, source); const authHeaders = sourceImpl.getRequestAuthHeaders(); Object.assign(headers, authHeaders); } catch (error) { logger.warn(`Failed to get auth headers for ${sourceName}: ${error.message}`); } // Get timeout from source config or use default const timeout = source.requestTimeout || 30000; // fallback to 30 seconds const response = await fetch(url, { method: options.method || 'GET', headers, body: options.body, timeout, ...options, }); const duration = Date.now() - startTime; const statusCode = response.status; const success = response.ok; logger.httpRequest(options.method || 'GET', url, statusCode, duration); if (!success) { // Check for non-retryable authentication/authorization errors first if (statusCode === 401) { throw new Error(`Authentication failed for ${sourceName}. Check API key.`); } else if (statusCode === 403) { throw new Error(`Access forbidden for ${sourceName}. Check permissions.`); } // Check if we should retry for this status code if (retryOnStatusCodes.includes(statusCode) && attempt < retryAttempts) { const error = new Error(`Request failed for ${sourceName}: ${statusCode}`); lastError = error; logger.warn(`Retryable error for ${sourceName}`, { statusCode, attempt, willRetry: true, }); continue; // Retry } // Non-retryable errors or max retries reached if (statusCode >= 500) { throw new Error(`Server error from ${sourceName}: ${statusCode}`); } else { throw new Error(`Request failed for ${sourceName}: ${statusCode}`); } } // Try to parse JSON response let data; try { data = await response.json(); } catch (jsonError) { // If JSON parsing fails, throw an error without retrying throw new Error(`Invalid JSON response from ${sourceName}: ${jsonError.message}`); } // Log successful retry if this was a retry attempt if (isRetry) { logger.info(`Request succeeded for ${sourceName} on retry attempt ${attempt}`, { url, statusCode, duration, }); } return { data, success: true, statusCode, }; } catch (error) { const duration = Date.now() - startTime; lastError = error; // Check if this is a network error that should be retried const isNetworkError = error instanceof TypeError || error instanceof Error && (error.message.includes('fetch') || error.message.includes('network') || error.message.includes('timeout') || error.message.includes('ECONNRESET') || error.message.includes('ENOTFOUND') || error.message.includes('ETIMEDOUT')); if (isNetworkError && attempt < retryAttempts) { logger.warn(`Network error for ${sourceName}, will retry`, { error: lastError.message, attempt, duration, url, }); continue; // Retry network errors } // Non-retryable error or max retries reached logger.error(`Request failed for ${sourceName}`, lastError, { url, duration, attempt, maxAttempts: retryAttempts, }); // If we've exhausted retries, throw the last error if (attempt === retryAttempts) { throw lastError; } // For non-network errors, don't retry throw lastError; } } // This should never be reached, but just in case throw lastError || new Error(`Request failed for ${sourceName} after ${retryAttempts} attempts`); } // Get the best available source for a specific operation getBestSourceForOperation(operation) { const enabledSources = this.getEnabledSources(); for (const source of enabledSources) { if (source.features.includes(operation)) { return source; } } return null; } // Find source key by matching the source object findSourceKey(targetSource) { for (const [key, source] of Object.entries(this.sources)) { if (source === targetSource) { return key; } } return null; } // Fallback mechanism - try multiple sources (simplified) async makeRequestWithFallback(operation, requestBuilder, _maxRetries = 3) { const enabledSources = this.getEnabledSources().filter(s => s.features.includes(operation)); if (enabledSources.length === 0) { throw new Error(`No sources available for operation: ${operation}`); } let lastError = null; for (const source of enabledSources) { try { // Find the source key by matching the source object const sourceKey = this.findSourceKey(source); if (!sourceKey) { throw new Error(`Could not find source key for: ${source.name}`); } const { url, options } = requestBuilder(source); const result = await this.makeRequest(sourceKey, url, options); return { data: result.data, source: source.name, }; } catch (error) { lastError = error; logger.warn(`Request failed for source ${source.name}, trying next`, { error: lastError.message, operation, }); continue; } } throw lastError || new Error(`All sources failed for operation: ${operation}`); } // Get source health status async getSourceHealth() { const health = {}; for (const [name, source] of Object.entries(this.sources)) { if (!source.enabled) { health[name] = { status: 'disabled' }; continue; } health[name] = { status: 'available', features: source.features, hasApiKey: this.checkIfSourceHasApiKey(source), }; } return health; } checkIfSourceHasApiKey(source) { try { // Try to create source implementation and check if it has an API key const sourceImpl = SourceFactory.createSourceImplementation('temp', source); const authHeaders = sourceImpl.getRequestAuthHeaders(); return Object.keys(authHeaders).length > 0; } catch { return false; } } } //# sourceMappingURL=source-manager.js.map