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