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