UNPKG

quality-mcp

Version:

An MCP server that analyzes to your codebase, with plugin support for DCD and Simian. 🏍️ "The only Zen you find on the tops of mountains is the Zen you bring up there."

768 lines (678 loc) 22.6 kB
/** * Configuration Validation and Security Checks * Comprehensive validation system for MCP server configuration */ import { existsSync } from 'fs'; import { tmpdir } from 'os'; import { resolve } from 'path'; import { validatePath, getSecurityConfig } from './security.js'; import { createLogger } from './logger.js'; const logger = createLogger('config-validator'); const getDeps = () => { return { logger, ConfigValidationError, }; }; /** * Configuration validation error */ export class ConfigValidationError extends Error { constructor(message, field = null, value = null) { super(message); this.name = 'ConfigValidationError'; this.field = field; this.value = value; } } /** * Configuration schema definitions */ const CONFIG_SCHEMA = { server: { name: { type: 'string', required: true, minLength: 1, maxLength: 100 }, version: { type: 'string', required: true }, // No version format restrictions - sanitize if needed timeout: { type: 'integer', min: 1000, max: 300000, default: 30000 }, }, plugins: { dcd: { enabled: { type: 'boolean', default: true }, executable: { type: 'string', default: 'dcd', validator: 'executableName' }, defaultMatchLength: { type: 'integer', min: 1, max: 1000, default: 6 }, defaultFuzziness: { type: 'integer', min: 0, max: 255, default: 0 }, timeout: { type: 'integer', min: 1000, max: 300000, default: 30000 }, cache: { enabled: { type: 'boolean', default: true }, directory: { type: 'string', validator: 'directoryPath' }, ttl: { type: 'integer', min: 0, max: 86400, default: 3600 }, }, supportedExtensions: { type: 'array', items: { type: 'string', pattern: /^\.[a-zA-Z0-9]+$/ }, }, }, simian: { enabled: { type: 'boolean', default: true }, executable: { type: 'string', validator: 'executableName' }, defaultThreshold: { type: 'integer', min: 2, max: 1000, default: 6 }, defaultFormatter: { type: 'string', enum: ['xml', 'plain', 'emacs', 'vs', 'yaml', 'text'], default: 'xml', }, timeout: { type: 'integer', min: 1000, max: 300000, default: 30000 }, javaExecutable: { type: 'string', default: 'java', validator: 'executableName' }, javaOptions: { type: 'array', items: { type: 'string' }, default: ['-jar'] }, cache: { enabled: { type: 'boolean', default: true }, directory: { type: 'string', validator: 'directoryPath' }, ttl: { type: 'integer', min: 0, max: 86400, default: 3600 }, }, supportedExtensions: { type: 'array', items: { type: 'string', pattern: /^\.[a-zA-Z0-9]+$/ }, }, options: { ignoreStrings: { type: 'boolean', default: false }, ignoreNumbers: { type: 'boolean', default: false }, ignoreCharacters: { type: 'boolean', default: false }, ignoreCurlyBraces: { type: 'boolean', default: false }, ignoreIdentifiers: { type: 'boolean', default: false }, }, }, }, cache: { enabled: { type: 'boolean', default: true }, directory: { type: 'string', default: './cache', validator: 'directoryPath' }, maxAge: { type: 'integer', min: 0, max: 86400000, default: 3600000 }, ttl: { type: 'integer', min: 0, max: 86400, default: 3600 }, }, security: { enableValidation: { type: 'boolean', default: true }, pathRestrictions: { type: 'boolean', default: true }, maxFileSize: { type: 'integer', min: 1, max: 1073741824, default: 10485760 }, }, logging: { level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'], default: 'info' }, format: { type: 'string', enum: ['text', 'json'], default: 'text' }, colors: { type: 'boolean', default: true }, }, development: { mockMode: { type: 'boolean', default: false }, testDataPath: { type: 'string', validator: 'directoryPath' }, }, }; /** * Security validation rules */ const SECURITY_RULES = { // Blocked executable names for security blockedExecutableNames: [ 'rm', 'rmdir', 'del', 'format', 'fdisk', 'mkfs', 'dd', 'wget', 'curl', 'nc', 'netcat', 'telnet', 'ssh', 'scp', 'rsync', 'sudo', 'su', ], // Dangerous path patterns dangerousPathPatterns: [ /\/etc\//, /\/proc\//, /\/sys\//, /\/dev\//, /\/root\//, /\\Windows\\/, /\\System32\\/, /\\Program Files\\/, /^\/bin\//, /^\/sbin\//, // Allow /usr/local/bin/ but block other system bins /^\/usr\/bin\//, /^\/usr\/sbin\//, ], // Maximum allowed timeout values maxTimeout: 300000, // 5 minutes // Maximum cache TTL maxCacheTtl: 86400, // 24 hours // Maximum string lengths maxStringLength: 1000, }; /** * Custom validators for specific field types */ const CUSTOM_VALIDATORS = { executable: (value, _field) => { if (!value) { return { valid: true }; } // Optional field try { // Always resolve path to absolute form for security checking const resolvedPath = resolve(value); // Check for dangerous patterns (MORE STRICT - check all paths) for (const pattern of SECURITY_RULES.dangerousPathPatterns) { if (pattern.test(resolvedPath)) { return { valid: false, error: `Executable path matches dangerous pattern: ${value}`, }; } } // Check for path traversal attacks (MORE STRICT) if (value.includes('..') || value.includes('~')) { return { valid: false, error: `Executable path contains path traversal: ${value}`, }; } // Additional security check for existing files if (existsSync(resolvedPath)) { const securityConfig = getSecurityConfig(process.env.NODE_ENV || 'development'); validatePath(resolvedPath, securityConfig); } return { valid: true, sanitizedValue: resolvedPath }; } catch (error) { return { valid: false, error: `Invalid executable path: ${error.message}`, }; } }, directoryPath: (value, _field) => { if (!value) { return { valid: true }; } // Optional field try { // Resolve the path even if it doesn't exist const resolvedPath = resolve(value); // Check for dangerous patterns for (const pattern of SECURITY_RULES.dangerousPathPatterns) { if (pattern.test(resolvedPath)) { return { valid: false, error: `Directory path matches dangerous pattern: ${value}`, }; } } return { valid: true, sanitizedValue: resolvedPath }; } catch (error) { return { valid: false, error: `Invalid directory path: ${error.message}`, }; } }, executableName: (value, _field) => { if (!value) { return { valid: true }; } // Optional field // Extract executable name from path if it's a full path const execName = value.toLowerCase().replace(/\.(exe|bat|cmd)$/, ''); const baseName = execName.split('/').pop(); // Get just the filename part // Check against blocked executable names if (SECURITY_RULES.blockedExecutableNames.includes(baseName)) { return { valid: false, error: `Executable name '${baseName}' is blocked for security reasons`, }; } // Basic name validation - allow both simple names and full paths if (!/^[a-zA-Z0-9._/-]+$/.test(value)) { return { valid: false, error: `Invalid executable name format: ${value}`, }; } return { valid: true }; }, }; /** * Validate a single configuration value against its schema * @param {any} value - Value to validate * @param {Object} schema - Schema definition * @param {string} fieldPath - Field path for error reporting * @returns {Object} Validation result */ function validateField(value, schema, fieldPath) { // Handle undefined/null values - be strict about nulls if (value === undefined) { if (schema.required) { return { valid: false, error: `Required field '${fieldPath}' is missing`, field: fieldPath, }; } return { valid: true, sanitizedValue: schema.default }; } // Reject explicit null values (more strict) if (value === null) { return { valid: false, error: `Field '${fieldPath}' cannot be null`, field: fieldPath, }; } // Type validation if (schema.type === 'string' && typeof value !== 'string') { return { valid: false, error: `Field '${fieldPath}' must be a string, got ${typeof value}`, field: fieldPath, }; } if (schema.type === 'integer' && (!Number.isInteger(value) || typeof value !== 'number')) { return { valid: false, error: `Field '${fieldPath}' must be an integer, got ${typeof value}`, field: fieldPath, }; } if (schema.type === 'boolean' && typeof value !== 'boolean') { return { valid: false, error: `Field '${fieldPath}' must be a boolean, got ${typeof value}`, field: fieldPath, }; } if (schema.type === 'array' && !Array.isArray(value)) { // Auto-wrap single values in arrays (more forgiving) if (value !== null && (typeof value === 'object' || typeof value === 'string')) { value = [value]; logger.warn(`Auto-wrapped single ${typeof value} in array for field '${fieldPath}'`); } else { return { valid: false, error: `Field '${fieldPath}' must be an array, got ${typeof value}`, field: fieldPath, }; } } // String validations if (schema.type === 'string') { if (schema.minLength && value.length < schema.minLength) { return { valid: false, error: `Field '${fieldPath}' must be at least ${schema.minLength} characters`, field: fieldPath, }; } if (schema.maxLength && value.length > schema.maxLength) { return { valid: false, error: `Field '${fieldPath}' must be at most ${schema.maxLength} characters`, field: fieldPath, }; } if (schema.pattern && !schema.pattern.test(value)) { return { valid: false, error: `Field '${fieldPath}' does not match required pattern`, field: fieldPath, }; } if (schema.enum && !schema.enum.includes(value)) { return { valid: false, error: `Field '${fieldPath}' must be one of: ${schema.enum.join(', ')}`, field: fieldPath, }; } // Security check for string length if (value.length > SECURITY_RULES.maxStringLength) { return { valid: false, error: `Field '${fieldPath}' exceeds maximum allowed length (${SECURITY_RULES.maxStringLength})`, field: fieldPath, }; } } // Number validations if (schema.type === 'integer') { if (schema.min !== undefined && value < schema.min) { return { valid: false, error: `Field '${fieldPath}' must be at least ${schema.min}`, field: fieldPath, }; } if (schema.max !== undefined && value > schema.max) { // Special message for file size limits const isFileSize = fieldPath.includes('FileSize') || fieldPath.includes('maxFileSize'); const errorMsg = isFileSize ? `Field '${fieldPath}' size limit exceeded: ${value} > ${schema.max} (max 1GB)` : `Field '${fieldPath}' must be at most ${schema.max}`; return { valid: false, error: errorMsg, field: fieldPath, }; } } // Array validations if (schema.type === 'array' && schema.items) { for (let i = 0; i < value.length; i++) { const itemResult = validateField(value[i], schema.items, `${fieldPath}[${i}]`); if (!itemResult.valid) { return itemResult; } } } // Custom validator if (schema.validator && CUSTOM_VALIDATORS[schema.validator]) { const customResult = CUSTOM_VALIDATORS[schema.validator](value, fieldPath); if (!customResult.valid) { return { valid: false, error: customResult.error, field: fieldPath, }; } // Use sanitized value if provided if (customResult.sanitizedValue !== undefined) { return { valid: true, sanitizedValue: customResult.sanitizedValue }; } } return { valid: true, sanitizedValue: value }; } /** * Validate configuration object recursively * @param {Object} config - Configuration to validate * @param {Object} schema - Schema definition * @param {string} basePath - Base path for error reporting * @returns {Object} Validation result with sanitized config */ function validateConfigSection(config, schema, basePath = '') { const sanitizedConfig = {}; const errors = []; // Validate each schema field for (const [key, fieldSchema] of Object.entries(schema)) { const fieldPath = basePath ? `${basePath}.${key}` : key; if ( typeof fieldSchema === 'object' && fieldSchema.type === undefined && !fieldSchema.validator ) { // Nested object - recurse const nestedResult = validateConfigSection(config[key] || {}, fieldSchema, fieldPath); if (nestedResult.errors.length > 0) { errors.push(...nestedResult.errors); } else { sanitizedConfig[key] = nestedResult.config; } } else { // Single field const result = validateField(config[key], fieldSchema, fieldPath); if (result.valid) { if (result.sanitizedValue !== undefined) { sanitizedConfig[key] = result.sanitizedValue; } } else { errors.push({ field: result.field, error: result.error, value: config[key], }); } } } // Check for unknown fields (potential typos or misconfigurations) for (const key of Object.keys(config)) { if (!Object.prototype.hasOwnProperty.call(schema, key)) { logger.warn(`Unknown configuration field: ${basePath ? `${basePath}.${key}` : key}`); } } return { config: sanitizedConfig, errors }; } /** * Perform security checks on configuration * @param {Object} config - Configuration to check * @returns {Array} Array of security warnings/errors */ function performSecurityChecks(config) { const securityIssues = []; // Check for development mode in production if (process.env.NODE_ENV === 'production' && config.development?.mockMode) { securityIssues.push({ severity: 'error', message: 'Mock mode is enabled in production environment', field: 'development.mockMode', }); } // Check for insecure timeout values if (config.server?.timeout > SECURITY_RULES.maxTimeout) { securityIssues.push({ severity: 'warning', message: `Server timeout (${config.server.timeout}ms) exceeds recommended maximum (${SECURITY_RULES.maxTimeout}ms)`, field: 'server.timeout', }); } // Check for debug logging in production if (process.env.NODE_ENV === 'production' && config.logging?.level === 'debug') { securityIssues.push({ severity: 'warning', message: 'Debug logging is enabled in production (may expose sensitive information)', field: 'logging.level', }); } return securityIssues; } /** * Validate interdependent configuration fields and apply fixes * @param {Object} config - Configuration to check and fix * @returns {Array} Array of warnings/errors for interdependent fields */ function validateInterdependentFields(config) { const issues = []; // Fix: Cache enabled but no directory - log warning and use temp dir if ( config.cache?.enabled === true && (!config.cache.directory || config.cache.directory.trim() === '') ) { config.cache.directory = `${tmpdir()}/quality-mcp-cache`; issues.push({ severity: 'warning', message: `Cache enabled without directory. Using temporary directory: ${config.cache.directory}`, field: 'cache.directory', }); } // Error: Plugin enabled but no executable path if (config.plugins) { for (const [pluginName, pluginConfig] of Object.entries(config.plugins)) { if ( pluginConfig?.enabled === true && (!pluginConfig.executable || pluginConfig.executable.trim() === '') ) { issues.push({ severity: 'error', message: `Plugin '${pluginName}' is enabled but has no executable path`, field: `plugins.${pluginName}.executable`, }); } } } return issues; } /** * Main configuration validation function * @param {Object} config - Configuration to validate * @param {Object} options - Validation options * @param {Object} _getDeps - Dependency injection function * @returns {Object} Validation result */ export function validateConfiguration(config, options = {}, _getDeps = getDeps) { const { securityChecks = true } = options; const { ConfigValidationError, logger } = _getDeps(); logger.info('Validating configuration...'); try { // Validate against schema const validationResult = validateConfigSection(config, CONFIG_SCHEMA); if (validationResult.errors.length > 0) { const errorMessage = validationResult.errors .map(err => { return `${err.field}: ${err.error}`; }) .join('\n'); throw new ConfigValidationError( `Configuration validation failed:\n${errorMessage}`, validationResult.errors[0].field, validationResult.errors[0].value ); } const sanitizedConfig = validationResult.config; // Validate interdependent fields and apply fixes const interdependencyIssues = validateInterdependentFields(sanitizedConfig); // Handle interdependency errors const interdependencyErrors = interdependencyIssues.filter(issue => { return issue.severity === 'error'; }); if (interdependencyErrors.length > 0) { const errorMessage = interdependencyErrors .map(err => { return `${err.field}: ${err.message}`; }) .join('\n'); throw new ConfigValidationError( `Configuration interdependency validation failed:\n${errorMessage}`, interdependencyErrors[0].field ); } // Perform security checks let securityIssues = []; if (securityChecks) { securityIssues = performSecurityChecks(sanitizedConfig); // Handle security errors const securityErrors = securityIssues.filter(issue => { return issue.severity === 'error'; }); if (securityErrors.length > 0) { const errorMessage = securityErrors .map(err => { return `${err.field}: ${err.message}`; }) .join('\n'); throw new ConfigValidationError( `Security validation failed:\n${errorMessage}`, securityErrors[0].field ); } // Log security warnings const securityWarnings = securityIssues.filter(issue => { return issue.severity === 'warning'; }); for (const warning of securityWarnings) { logger.warn(`Security warning - ${warning.field}: ${warning.message}`); } } // Log interdependency warnings const interdependencyWarnings = interdependencyIssues.filter(issue => { return issue.severity === 'warning'; }); for (const warning of interdependencyWarnings) { logger.warn(`Configuration fix applied - ${warning.field}: ${warning.message}`); } logger.info('Configuration validation completed successfully'); const result = { valid: true, config: sanitizedConfig, securityIssues, warnings: securityIssues.filter(issue => { return issue.severity === 'warning'; }), }; return result; } catch (error) { logger.error('Configuration validation failed:', error); return { valid: false, error: error.message, field: error.field, value: error.value, }; } } // Environment-specific configuration schema (more flexible than main config) const ENV_CONFIG_SCHEMA = { development: { mockMode: { type: 'boolean', optional: true }, debugLogging: { type: 'boolean', optional: true }, enableHotReload: { type: 'boolean', optional: true }, }, production: { enableMetrics: { type: 'boolean', optional: true }, securityLevel: { type: 'string', enum: ['strict', 'normal', 'relaxed'], optional: true }, optimizePerformance: { type: 'boolean', optional: true }, }, logging: { level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'], optional: true }, format: { type: 'string', enum: ['text', 'json'], optional: true }, }, cache: { enabled: { type: 'boolean', optional: true }, directory: { type: 'string', optional: true }, maxAge: { type: 'number', min: 0, optional: true }, }, }; /** * Validate environment configuration and return detailed result * @param {Object} envConfig - Environment configuration * @returns {Object} Validation result with valid flag and config */ export function validateEnvironmentConfig(envConfig, _getDeps = getDeps) { const { logger } = _getDeps(); logger.debug('Validating environment configuration...'); try { // Handle empty config gracefully if (!envConfig || Object.keys(envConfig).length === 0) { return { valid: true, config: {}, warnings: [], }; } // Validate against environment schema (more lenient than main config) const validationResult = validateConfigSection(envConfig, ENV_CONFIG_SCHEMA, 'env'); if (validationResult.errors.length > 0) { const errorMessage = validationResult.errors .map(err => { return `${err.field}: ${err.error}`; }) .join('\n'); return { valid: false, error: `Environment configuration validation failed:\n${errorMessage}`, config: null, warnings: [], }; } logger.debug('Environment configuration validation completed successfully'); return { valid: true, config: validationResult.config, warnings: [], }; } catch (error) { logger.warn(`Environment configuration validation failed: ${error.message}`); return { valid: false, error: error.message, config: null, warnings: [], }; } } /** * Export schema for external use */ export { CONFIG_SCHEMA, SECURITY_RULES };