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