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

383 lines 16 kB
import { BaseCVESourceImplementation } from './base.js'; import { analyzeGitHub403Error, formatRateLimitReset } from '../utils/github-api.js'; import { createContextLogger } from '../utils/logger.js'; import { normalizeDescriptions } from '../utils/cve-utils.js'; const logger = createContextLogger('GitHubCVESource'); // GitHub Source Implementation export class GitHubSourceImplementation extends BaseCVESourceImplementation { buildSearchRequest(filters) { const params = new URLSearchParams(); // GitHub Security Advisories API parameters if (filters.cveId) { params.append('cve_id', filters.cveId); } if (filters.keyword) { // GitHub API uses 'q' for keyword search params.append('q', filters.keyword); } if (filters.cvssV3Severity) { params.append('severity', filters.cvssV3Severity.toLowerCase()); } if (filters.pubStartDate) { params.append('published', `>=${filters.pubStartDate}`); } if (filters.pubEndDate) { params.append('published', `<=${filters.pubEndDate}`); } if (filters.resultsPerPage) { // GitHub API limits to 100 per page params.append('per_page', Math.min(filters.resultsPerPage, 100).toString()); } else { params.append('per_page', '20'); } // GitHub uses 'page' instead of startIndex if (filters.startIndex) { const page = Math.floor(filters.startIndex / (filters.resultsPerPage || 20)) + 1; params.append('page', page.toString()); } // Default sort by published date params.append('sort', 'published'); params.append('direction', 'desc'); const url = `${this.getBaseUrl()}?${params.toString()}`; return { url, options: this.getRequestOptions() }; } buildDetailsRequest(cveId) { // GitHub doesn't have direct CVE lookup, search by CVE ID const params = new URLSearchParams(); params.append('cve_id', cveId); params.append('per_page', '1'); const url = `${this.getBaseUrl()}?${params.toString()}`; return { url, options: this.getRequestOptions() }; } normalizeSearchResults(data) { const advisories = Array.isArray(data) ? data : []; return { cves: advisories.map((advisory) => this.normalizeCVEData(advisory)), totalResults: advisories.length, // GitHub doesn't provide total count in all responses startIndex: 0, // GitHub uses page-based pagination resultsPerPage: advisories.length, format: 'GitHub_Advisory', version: '1.0', timestamp: new Date().toISOString(), }; } normalizeCVEData(data) { const advisory = data; // Handle case where GitHub returns an empty array or invalid data if (Array.isArray(data)) { logger.warn('GitHub returned array data to normalizeCVEData, expected single advisory object'); throw new Error('CVE not found in GitHub Security Advisories'); } if (!advisory || typeof advisory !== 'object') { logger.warn('GitHub returned invalid advisory data', { dataType: typeof advisory, isNull: advisory === null, isUndefined: advisory === undefined, }); throw new Error('CVE not found in GitHub Security Advisories: invalid data'); } // Check if we have minimal required fields for a valid advisory if (!advisory.cve_id && !advisory.ghsa_id) { logger.warn('GitHub advisory missing both CVE ID and GHSA ID', { advisoryKeys: Object.keys(advisory), }); throw new Error('CVE not found in GitHub Security Advisories: missing identifiers'); } const normalizedReferences = this.normalizeGitHubReferences(advisory); return { id: advisory.cve_id || advisory.ghsa_id || 'unknown', sourceIdentifier: 'github.com', published: advisory.published_at || new Date().toISOString(), lastModified: advisory.updated_at || new Date().toISOString(), vulnStatus: this.mapGitHubState(advisory.state), cveTags: advisory.tags || [], descriptions: normalizeDescriptions([{ lang: 'en', value: advisory.description || advisory.summary || 'No description found', }]), metrics: this.normalizeGitHubMetrics(advisory), weaknesses: this.normalizeGitHubWeaknesses(advisory.cwe_ids), configurations: [], // GitHub doesn't provide CPE configurations references: normalizedReferences, // Calculate exploit indicators during normalization exploitIndicators: this.calculateExploitIndicators(normalizedReferences), dataSource: { name: 'github', version: '2.0', lastUpdated: new Date().toISOString(), url: this.getBaseUrl(), }, processingInfo: { extractedAt: new Date().toISOString(), normalizedBy: 'github_source', rawDataAvailable: true, }, }; } mapGitHubState(state) { switch (state?.toLowerCase()) { case 'published': return 'PUBLISHED'; case 'withdrawn': return 'REJECTED'; case 'draft': return 'RESERVED'; default: return 'UNKNOWN'; } } normalizeGitHubMetrics(advisory) { if (!advisory.cvss) { return undefined; } const cvssData = { version: '3.1', vectorString: String(advisory.cvss?.vector_string || ''), baseScore: Number(advisory.cvss?.score || 0), baseSeverity: (advisory.severity || 'UNKNOWN').toUpperCase(), // GitHub doesn't provide detailed CVSS breakdown, use defaults attackVector: 'UNKNOWN', attackComplexity: 'UNKNOWN', privilegesRequired: 'UNKNOWN', userInteraction: 'UNKNOWN', scope: 'UNKNOWN', confidentialityImpact: 'UNKNOWN', integrityImpact: 'UNKNOWN', availabilityImpact: 'UNKNOWN', }; return { cvssMetricV31: [{ source: 'github.com', type: 'Primary', cvssData, }], }; } normalizeGitHubWeaknesses(cweIds) { if (!Array.isArray(cweIds)) { return []; } return cweIds.map(cweId => ({ source: 'github.com', type: 'Primary', description: [{ lang: 'en', value: `CWE-${cweId}`, }], })); } normalizeGitHubReferences(advisory) { const references = []; // Add advisory URL if (advisory.html_url) { references.push({ url: advisory.html_url, source: 'github.com', tags: ['Vendor Advisory'], name: 'GitHub Security Advisory', refsource: 'GITHUB', }); } // Add references from the advisory if (Array.isArray(advisory.references)) { references.push(...advisory.references.map((ref) => ({ url: ref.url || ref, source: 'github.com', tags: ['Third Party Advisory'], name: ref.name || ref.title || undefined, refsource: ref.refsource || 'MISC', }))); } return references; } async testConnection() { const testUrl = `${this.getBaseUrl()}?per_page=1`; const response = await this.makeGitHubApiRequest(testUrl, { signal: AbortSignal.timeout(5000), }); return response !== null && response.ok; } // GitHub-specific validation methods /** * Validates GitHub-specific configuration */ validateSourceSpecificConfig() { const errors = []; const warnings = []; // Validate GitHub-specific endpoints if (this.config.endpoints?.search) { const searchUrl = this.config.endpoints.search; if (!searchUrl.includes('api.github.com') && !searchUrl.includes('localhost')) { warnings.push(`GitHub source ${this.config.name}: search endpoint doesn't appear to be official GitHub API`); } } if (this.config.endpoints?.details) { const detailsUrl = this.config.endpoints.details; if (!detailsUrl.includes('api.github.com') && !detailsUrl.includes('localhost')) { warnings.push(`GitHub source ${this.config.name}: details endpoint doesn't appear to be official GitHub API`); } } // Validate GitHub-specific auth configuration if (this.config.authHeaderType && !['bearer', 'token'].includes(this.config.authHeaderType)) { const authTypeMsg = `GitHub source ${this.config.name}: GitHub API typically uses ` + `'bearer' or 'token' auth type, found '${this.config.authHeaderType}'`; warnings.push(authTypeMsg); } if (this.config.authHeaderName && this.config.authHeaderName !== 'Authorization') { const headerNameMsg = `GitHub source ${this.config.name}: GitHub API uses ` + `'Authorization' header name, found '${this.config.authHeaderName}'`; warnings.push(headerNameMsg); } // Check required features for GitHub const requiredFeatures = ['security-advisories']; for (const feature of requiredFeatures) { if (!this.config.features?.includes(feature)) { warnings.push(`GitHub source ${this.config.name}: missing recommended feature '${feature}'`); } } return { isValid: errors.length === 0, errors, warnings }; } /** * Validates GitHub token format and configuration */ validateSourceSpecificApiKey() { const errors = []; const warnings = []; if (this.apiKey) { // GitHub tokens should start with specific prefixes const validPrefixes = ['ghp_', 'github_pat_', 'gho_', 'ghu_', 'ghs_', 'ghr_']; const hasValidPrefix = validPrefixes.some(prefix => this.apiKey && this.apiKey.startsWith(prefix)); if (!hasValidPrefix) { const tokenFormatMsg = `GitHub source ${this.config.name}: token format may be ` + 'incorrect (should start with ghp_, github_pat_, etc.)'; warnings.push(tokenFormatMsg); } if (this.apiKey.length < 20) { warnings.push(`GitHub source ${this.config.name}: token appears to be too short`); } // Check for classic vs fine-grained tokens if (this.apiKey.startsWith('github_pat_')) { // Fine-grained personal access token const fineGrainedMsg = `GitHub source ${this.config.name}: using fine-grained token ` + '- ensure it has security_events:read permission'; warnings.push(fineGrainedMsg); } else if (this.apiKey.startsWith('ghp_')) { // Classic personal access token warnings.push(`GitHub source ${this.config.name}: using classic token - ensure it has security_events scope`); } } return { isValid: errors.length === 0, errors, warnings }; } /** * Validates GitHub-specific rate limiting configuration */ /** * GitHub-specific API key environment variable from configuration */ getApiKeyEnvironmentVariable() { return this.config.apiKeyEnvVar || 'GITHUB_TOKEN'; } /** * Alternative environment variables for GitHub token */ getAlternativeApiKeyEnvironmentVariables() { return ['GITHUB_API_KEY', 'GH_TOKEN']; // Alternative names sometimes used } /** * GitHub API key identifier for mapping */ getApiKeyIdentifier() { return 'github'; } /** * Check if this source matches GitHub-related identifiers */ matchesSourceIdentifier(identifier) { const lowerIdentifier = identifier.toLowerCase(); return lowerIdentifier === 'github' || lowerIdentifier === 'gh' || lowerIdentifier.includes('github') || super.matchesSourceIdentifier(identifier); } /** * Validate if the API key can be used by GitHub */ canUseApiKey(apiKey) { if (!apiKey) { return false; } // GitHub tokens should start with specific prefixes and be long enough const validPrefixes = ['ghp_', 'github_pat_', 'gho_', 'ghu_', 'ghs_', 'ghr_']; const hasValidPrefix = validPrefixes.some(prefix => apiKey.startsWith(prefix)); return hasValidPrefix && apiKey.length >= 20; } /** * Override base class authentication for GitHub-specific Bearer token */ getAuthHeaders() { const headers = {}; if (this.apiKey) { headers['Authorization'] = `Bearer ${this.apiKey}`; logger.debug('GitHub authentication headers configured'); } return headers; } getRequestOptions() { const headers = { 'Accept': 'application/vnd.github+json', 'User-Agent': this.getUserAgent(), 'X-GitHub-Api-Version': '2022-11-28', ...this.getAuthHeaders(), }; return { headers, timeout: this.getTimeout(), }; } /** * Makes a GitHub API request with proper error handling for rate limits and permissions */ async makeGitHubApiRequest(url, options = {}) { try { const response = await fetch(url, { ...this.getRequestOptions(), ...options, }); if (!response.ok) { if (response.status === 403) { const errorInfo = analyzeGitHub403Error(response); if (errorInfo.isRateLimit) { const resetInfo = errorInfo.resetTime ? ` (resets in ${formatRateLimitReset(errorInfo.resetTime)})` : ''; logger.warn(`GitHub CVE source: ${errorInfo.message}${resetInfo}`, { source: 'github-cve', limitType: errorInfo.limitType, resetTime: errorInfo.resetTime, url: url.replace(/[?&]access_token=[^&]+/g, ''), // Remove token from logs }); } else { logger.warn(`GitHub CVE source: ${errorInfo.message}`, { source: 'github-cve', url: url.replace(/[?&]access_token=[^&]+/g, ''), }); } return null; } logger.warn(`GitHub CVE source: API responded with status ${response.status}`, { source: 'github-cve', status: response.status, url: url.replace(/[?&]access_token=[^&]+/g, ''), }); return null; } return response; } catch (error) { logger.warn('GitHub CVE source: Request failed', { source: 'github-cve', error: error instanceof Error ? error.message : String(error), url: url.replace(/[?&]access_token=[^&]+/g, ''), }); return null; } } } //# sourceMappingURL=github.js.map