UNPKG

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

411 lines 15.6 kB
/** * CVE and data validation utilities */ import { getAppConfiguration } from '../config/index.js'; import { createContextLogger } from './logger.js'; const logger = createContextLogger('ValidationUtils'); /** * Infrastructure-focused input sanitization for CVE intelligence server * ONLY removes characters that could damage the server infrastructure * Preserves ALL vulnerability data - attackers can attack APIs directly anyway */ export function sanitizeInput(input, maxLength = 1000) { if (!input || typeof input !== 'string') { return ''; } // Ultra-minimal sanitization - protect infrastructure, preserve data const sanitized = input .substring(0, maxLength) // Only remove NULL bytes - these can cause Node.js issues .replace(/\0/g, '') .trim(); return sanitized; } /** * Validates and sanitizes CVE ID with enhanced checks */ export function validateAndSanitizeCVEId(cveId) { if (!cveId || typeof cveId !== 'string') { throw new Error('CVE ID must be a non-empty string'); } // Sanitize input first const sanitized = sanitizeInput(cveId, 20).toUpperCase().trim(); // Validate format if (!isValidCVEId(sanitized)) { throw new Error(`Invalid CVE ID format: ${sanitized}. Expected format: CVE-YYYY-NNNN`); } return sanitized; } /** * Validates and sanitizes search parameters * For MCP JSON-RPC requests - parameter names come from SDK and are safe */ export function validateSearchParameters(params) { if (!params || typeof params !== 'object') { logger.debug('validateSearchParameters: invalid input, returning empty object', { params, type: typeof params, }); return {}; } const sanitized = {}; try { for (const [key, value] of Object.entries(params)) { // MCP parameter names come from SDK - only basic validation needed if (!key || typeof key !== 'string' || key.length > 100) { continue; // Skip invalid parameter names } // Handle different value types - preserve CVE content if (typeof value === 'string') { sanitized[key] = sanitizeInput(value, 1000); } else if (typeof value === 'number') { // Validate numeric ranges if (isFinite(value) && value >= 0 && value <= 1000000) { sanitized[key] = value; } } else if (typeof value === 'boolean') { sanitized[key] = value; } else if (Array.isArray(value)) { // Sanitize array elements - preserve CVE content sanitized[key] = value .filter(item => typeof item === 'string') .map(item => sanitizeInput(item, 200)) .filter(item => item.length > 0) .slice(0, 10); // Limit array size } } return sanitized; } catch (error) { logger.warn('Error validating search parameters', { error: error instanceof Error ? error.message : String(error), }); return {}; } } /** * Type guard to check if a value is a non-empty string */ export function isNonEmptyString(value) { return typeof value === 'string' && value.trim().length > 0; } /** * Type guard to check if a value is a valid number */ export function isValidNumber(value) { return typeof value === 'number' && !isNaN(value) && isFinite(value); } /** * Type guard to check if a value is a non-empty array */ export function isNonEmptyArray(value) { return Array.isArray(value) && value.length > 0; } /** * Safely converts a value to string with fallback */ export function safeString(value, fallback = '') { if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } return fallback; } /** * Safely converts a value to number with fallback */ export function safeNumber(value, fallback = 0) { if (typeof value === 'number' && !isNaN(value) && isFinite(value)) { return value; } if (typeof value === 'string') { const parsed = parseFloat(value); if (!isNaN(parsed) && isFinite(parsed)) { return parsed; } } return fallback; } /** * Validates CVE ID format * @param cveId CVE identifier to validate * @returns true if valid CVE format */ export function isValidCVEId(cveId) { if (!cveId || typeof cveId !== 'string') { logger.debug('CVE ID validation failed: invalid input', { cveId, type: typeof cveId }); return false; } // CVE format: CVE-YYYY-NNNN where YYYY is year and NNNN is at least 4 digits const cvePattern = /^CVE-\d{4}-\d{4,}$/; const isValid = cvePattern.test(cveId); if (!isValid) { logger.debug('CVE ID validation failed: invalid format', { cveId }); } return isValid; } /** * Normalizes and deduplicates CVE IDs (case-insensitive) * @param cveIds Array of CVE identifiers * @returns Array of normalized unique CVE IDs */ export function normalizeAndDeduplicateCVEIds(cveIds) { if (!Array.isArray(cveIds)) { logger.warn('normalizeAndDeduplicateCVEIds: input is not an array', { input: cveIds }); return []; } const normalized = new Set(); const invalidIds = []; for (const cveId of cveIds) { if (!cveId || typeof cveId !== 'string') { invalidIds.push(String(cveId)); continue; } // Normalize to uppercase for consistency const normalizedId = cveId.toUpperCase().trim(); if (!isValidCVEId(normalizedId)) { invalidIds.push(cveId); continue; } normalized.add(normalizedId); } if (invalidIds.length > 0) { logger.warn('normalizeAndDeduplicateCVEIds: found invalid CVE IDs', { invalidIds, total: cveIds.length, valid: normalized.size, }); } const result = Array.from(normalized); const duplicatesRemoved = cveIds.length - result.length; if (duplicatesRemoved > 0) { logger.debug('normalizeAndDeduplicateCVEIds: removed duplicates', { original: cveIds.length, unique: result.length, duplicatesRemoved, }); } return result; } /** * Validates date range parameters * @param startDate Start date string * @param endDate End date string * @returns true if valid date range */ export function isValidDateRange(startDate, endDate) { logger.debug('Validating date range', { startDate, endDate }); if (!startDate && !endDate) { logger.debug('Date range validation: both dates undefined (valid)'); return true; // Both undefined is valid } if (!startDate || !endDate) { logger.debug('Date range validation: open-ended range (valid)', { startDate, endDate }); return true; // Only one defined is valid (open-ended range) } try { const start = new Date(startDate); const end = new Date(endDate); // Check if dates are valid if (isNaN(start.getTime()) || isNaN(end.getTime())) { logger.debug('Date range validation failed: invalid date format', { startDate, endDate }); return false; } // Start should be before or equal to end const isValid = start <= end; if (!isValid) { logger.debug('Date range validation failed: start date after end date', { startDate, endDate, startTime: start.getTime(), endTime: end.getTime(), }); } return isValid; } catch (error) { logger.debug('Date range validation failed: exception', { startDate, endDate, error: error instanceof Error ? error.message : String(error), }); return false; } } /** * Parses CPE (Common Platform Enumeration) string * @param cpe CPE string to parse * @returns Parsed CPE components */ export function parseCPE(cpe) { logger.debug('Parsing CPE string', { cpe }); if (!cpe || typeof cpe !== 'string') { logger.debug('CPE parsing failed: invalid input', { cpe, type: typeof cpe }); return { vendor: '', product: '' }; } // Basic CPE 2.3 format: // cpe:2.3:part:vendor:product:version:update:edition:language:sw_edition:target_sw:target_hw:other const cpePattern = /^cpe:2\.3:([^:]*):([^:]*):([^:]*):([^:]*)/; const match = cpe.match(cpePattern); if (!match) { logger.debug('CPE does not match v2.3 format, trying simple format', { cpe }); // Try to extract basic info from simpler formats const simpleCpe = cpe.replace(/^cpe:/, '').split(':'); if (simpleCpe.length >= 3) { const result = { vendor: simpleCpe[1] || '', product: simpleCpe[2] || '', version: simpleCpe[3] && simpleCpe[3] !== '*' ? simpleCpe[3] : undefined, }; logger.debug('CPE parsed using simple format', { cpe, result }); return result; } logger.debug('CPE parsing failed: insufficient components', { cpe, components: simpleCpe.length }); return { vendor: '', product: '' }; } const result = { vendor: match[2] || '', product: match[3] || '', version: match[4] && match[4] !== '*' ? match[4] : undefined, }; logger.debug('CPE parsed successfully using v2.3 format', { cpe, result }); return result; } /** * Gets exploit indicator patterns from configuration * @returns Map of exploit indicator patterns */ export function getExploitIndicators() { logger.debug('Loading exploit indicator patterns from configuration'); try { const config = getAppConfiguration(); // Access exploit indicators from the app configuration const exploitConfig = config.exploitIndicators; if (!exploitConfig?.enabled) { logger.info('Exploit indicators are disabled in configuration'); return {}; } if (!exploitConfig.patterns) { logger.warn('Exploit indicators enabled but no patterns configured'); return {}; } logger.debug('Exploit indicators are enabled, building patterns', { hasPatterns: !!exploitConfig.patterns, hasCustomPatterns: !!exploitConfig.customPatterns, customPatternsCount: exploitConfig.customPatterns?.length || 0, }); const patterns = {}; // Build patterns from configuration if (Object.keys(exploitConfig.patterns).length > 0) { let builtPatterns = 0; for (const [key, pattern] of Object.entries(exploitConfig.patterns)) { const validDomains = pattern.domains.filter(d => d && d.trim()); const validPaths = pattern.paths.filter(p => p && p.trim()); const validKeywords = pattern.keywords.filter(k => k && k.trim()); const patternParts = []; if (validDomains.length > 0) { const domainPattern = validDomains.map(d => d.replace(/\./g, '\\.')).join('|'); patternParts.push(`(?:${domainPattern})`); } if (validPaths.length > 0) { const pathPattern = validPaths.join('|'); patternParts.push(`(?:${pathPattern})`); } if (validKeywords.length > 0) { const keywordPattern = validKeywords.join('|'); patternParts.push(`(?:${keywordPattern})`); } if (patternParts.length > 0) { const regexPattern = patternParts.join('|'); try { patterns[key] = new RegExp(regexPattern, 'i'); builtPatterns++; logger.debug('Built exploit pattern', { key, domainsCount: validDomains.length, pathsCount: validPaths.length, keywordsCount: validKeywords.length, pattern: regexPattern, }); } catch (error) { logger.warn(`Failed to build regex pattern for ${key}`, { key, pattern: regexPattern, error: error instanceof Error ? error.message : String(error), }); } } else { logger.debug('Skipping exploit pattern with no valid components', { key }); } } logger.debug('Built exploit patterns from configuration', { builtPatterns }); } // Add custom patterns if (exploitConfig.customPatterns && exploitConfig.customPatterns.length > 0) { let validCustomPatterns = 0; exploitConfig.customPatterns.forEach((pattern, index) => { try { patterns[`custom_${index}`] = new RegExp(pattern, 'i'); validCustomPatterns++; logger.debug('Added custom exploit pattern', { index, pattern }); } catch (error) { logger.warn(`Invalid custom exploit pattern at index ${index}: ${pattern}`, { pattern, index, error: error instanceof Error ? error.message : String(error), }); } }); logger.debug('Processed custom exploit patterns', { total: exploitConfig.customPatterns.length, valid: validCustomPatterns, invalid: exploitConfig.customPatterns.length - validCustomPatterns, }); } logger.debug('Exploit indicator patterns loaded successfully', { totalPatterns: Object.keys(patterns).length, }); return patterns; } catch (error) { logger.warn('Failed to load exploit indicators configuration', { error: error instanceof Error ? { message: error.message, name: error.name, stack: error.stack, } : String(error), }); return {}; } } /** * Validates and normalizes a required environment variable * @param name Environment variable name * @returns Environment variable value * @throws Error if variable is not set */ export function getRequiredEnvVar(name) { if (!name || typeof name !== 'string') { throw new Error('Environment variable name must be a non-empty string'); } logger.debug('Retrieving required environment variable', { name }); const value = process.env[name]; if (!value || value.trim().length === 0) { logger.error('Required environment variable not set or empty', undefined, { name }); throw new Error(`Required environment variable ${name} is not set or is empty`); } logger.debug('Required environment variable retrieved successfully', { name, hasValue: !!value, valueLength: value.length, }); return value; } //# sourceMappingURL=validation.js.map