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
JavaScript
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