UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

454 lines â€ĸ 18.3 kB
import { logger } from '../utils/logger.js'; export class ConfigValidator { constructor() { } static getInstance() { if (!ConfigValidator.instance) { ConfigValidator.instance = new ConfigValidator(); } return ConfigValidator.instance; } validateConfiguration(config, schema, options = {}) { const { strictMode = true, allowMissingOptional = true, validateCredentials = true, sanitizeSecrets = true } = options; const errors = []; const securityIssues = []; const sanitizedConfig = {}; try { // Validate each field in the schema for (const field of schema) { const value = this.getNestedValue(config, field.name); const fieldResult = this.validateField(field, value, { strictMode, allowMissingOptional, validateCredentials, sanitizeSecrets }); errors.push(...fieldResult.errors); if (fieldResult.securityIssues) { securityIssues.push(...fieldResult.securityIssues); } if (fieldResult.sanitizedValue !== undefined) { this.setNestedValue(sanitizedConfig, field.name, fieldResult.sanitizedValue); } } // Check for unknown fields in strict mode if (strictMode) { const unknownFields = this.findUnknownFields(config, schema); for (const unknownField of unknownFields) { errors.push({ field: unknownField, message: `Unknown configuration field '${unknownField}'`, severity: 'warning' }); } } // Validate environment variable security if (validateCredentials) { const envSecurityIssues = this.validateEnvironmentSecurity(); if (envSecurityIssues) { securityIssues.push(...envSecurityIssues); } } const isValid = errors.filter(e => e.severity === 'error').length === 0; return { isValid, errors, sanitizedConfig: isValid ? sanitizedConfig : undefined, securityIssues }; } catch (error) { logger.error('Configuration validation error:', error); return { isValid: false, errors: [{ field: 'global', message: `Configuration validation failed: ${error.message}`, severity: 'error' }] }; } } validateField(field, value, options) { const errors = []; const securityIssues = []; let sanitizedValue = value; // Check if required field is missing if (field.required && (value === undefined || value === null || value === '')) { errors.push({ field: field.name, message: `Required field '${field.name}' is missing`, severity: 'error' }); return { errors, securityIssues }; } // Use default value if field is missing and not required if ((value === undefined || value === null) && field.default !== undefined) { sanitizedValue = field.default; value = field.default; } // Skip validation if value is still undefined/null and field is optional if (value === undefined || value === null) { return { errors, securityIssues, sanitizedValue }; } // Type validation const typeValidation = this.validateType(field, value); if (!typeValidation.isValid) { errors.push({ field: field.name, message: typeValidation.message, severity: 'error' }); return { errors, securityIssues }; } sanitizedValue = typeValidation.sanitizedValue; // Field-specific validation if (field.validation) { const validationResult = this.validateFieldRules(field, sanitizedValue); errors.push(...validationResult.errors); } // Security validation for sensitive fields if (field.sensitive && options.validateCredentials) { const securityResult = this.validateSensitiveField(field, sanitizedValue); if (securityResult.securityIssues) { securityIssues.push(...securityResult.securityIssues); } if (options.sanitizeSecrets) { sanitizedValue = securityResult.sanitizedValue; } } return { errors, securityIssues, sanitizedValue }; } validateType(field, value) { switch (field.type) { case 'string': if (typeof value !== 'string') { if (typeof value === 'number' || typeof value === 'boolean') { return { isValid: true, message: '', sanitizedValue: String(value) }; } return { isValid: false, message: `Field '${field.name}' must be a string, got ${typeof value}`, sanitizedValue: value }; } break; case 'number': if (typeof value !== 'number') { if (typeof value === 'string' && !isNaN(Number(value))) { return { isValid: true, message: '', sanitizedValue: Number(value) }; } return { isValid: false, message: `Field '${field.name}' must be a number, got ${typeof value}`, sanitizedValue: value }; } if (isNaN(value) || !isFinite(value)) { return { isValid: false, message: `Field '${field.name}' must be a valid number`, sanitizedValue: value }; } break; case 'boolean': if (typeof value !== 'boolean') { if (value === 'true' || value === 'false') { return { isValid: true, message: '', sanitizedValue: value === 'true' }; } if (value === 1 || value === 0) { return { isValid: true, message: '', sanitizedValue: value === 1 }; } return { isValid: false, message: `Field '${field.name}' must be a boolean, got ${typeof value}`, sanitizedValue: value }; } break; case 'object': if (typeof value !== 'object' || value === null || Array.isArray(value)) { return { isValid: false, message: `Field '${field.name}' must be an object, got ${typeof value}`, sanitizedValue: value }; } break; case 'array': if (!Array.isArray(value)) { return { isValid: false, message: `Field '${field.name}' must be an array, got ${typeof value}`, sanitizedValue: value }; } break; } return { isValid: true, message: '', sanitizedValue: value }; } validateFieldRules(field, value) { const errors = []; const validation = field.validation; // String validations if (field.type === 'string' && typeof value === 'string') { if (validation.minLength && value.length < validation.minLength) { errors.push({ field: field.name, message: `Field '${field.name}' must be at least ${validation.minLength} characters long`, severity: 'error' }); } if (validation.maxLength && value.length > validation.maxLength) { errors.push({ field: field.name, message: `Field '${field.name}' must be at most ${validation.maxLength} characters long`, severity: 'error' }); } if (validation.pattern && !validation.pattern.test(value)) { errors.push({ field: field.name, message: `Field '${field.name}' does not match required pattern`, severity: 'error' }); } if (validation.enum && !validation.enum.includes(value)) { errors.push({ field: field.name, message: `Field '${field.name}' must be one of: ${validation.enum.join(', ')}`, severity: 'error' }); } } // Number validations if (field.type === 'number' && typeof value === 'number') { if (validation.min !== undefined && value < validation.min) { errors.push({ field: field.name, message: `Field '${field.name}' must be at least ${validation.min}`, severity: 'error' }); } if (validation.max !== undefined && value > validation.max) { errors.push({ field: field.name, message: `Field '${field.name}' must be at most ${validation.max}`, severity: 'error' }); } } // Custom validation if (validation.custom) { const customResult = validation.custom(value); if (!customResult.isValid) { errors.push({ field: field.name, message: customResult.message || `Field '${field.name}' failed custom validation`, severity: 'error' }); } } return { errors }; } validateSensitiveField(field, value) { const securityIssues = []; let sanitizedValue = value; if (typeof value === 'string') { // Check for weak credentials if (this.isWeakCredential(value)) { securityIssues.push({ field: field.name, issue: 'Weak credential detected', recommendation: 'Use a strong, randomly generated credential' }); } // Check for default/example credentials if (this.isDefaultCredential(value)) { securityIssues.push({ field: field.name, issue: 'Default or example credential detected', recommendation: 'Replace with a secure, unique credential' }); } // Check for exposed credentials if (this.isExposedCredential(value)) { securityIssues.push({ field: field.name, issue: 'Potentially exposed credential detected', recommendation: 'Rotate credential and review security practices' }); } // Sanitize for logging (mask most of the value) if (value.length > 8) { sanitizedValue = value.substring(0, 4) + '*'.repeat(value.length - 8) + value.substring(value.length - 4); } else if (value.length > 0) { sanitizedValue = '*'.repeat(value.length); } } return { securityIssues, sanitizedValue }; } isWeakCredential(value) { // Check for common weak patterns const weakPatterns = [ /^password$/i, /^123456/, /^admin$/i, /^test$/i, /^secret$/i, /^key$/i, /^token$/i ]; return weakPatterns.some(pattern => pattern.test(value)) || value.length < 8; } isDefaultCredential(value) { const defaultCredentials = [ 'default', 'example', 'sample', 'demo', 'placeholder', 'your_api_key_here', 'your_secret_here', 'change_me', 'replace_with_actual' ]; return defaultCredentials.some(def => value.toLowerCase().includes(def)); } isExposedCredential(value) { // Check for patterns that suggest exposed credentials const exposedPatterns = [ /github/i, /gitlab/i, /bitbucket/i, /leaked/i, /exposed/i, /public/i ]; return exposedPatterns.some(pattern => pattern.test(value)); } validateEnvironmentSecurity() { const securityIssues = []; // Check for environment variables that might contain sensitive data const sensitiveEnvVars = [ 'PASSWORD', 'SECRET', 'KEY', 'TOKEN', 'CREDENTIAL', 'AUTH' ]; for (const [key, value] of Object.entries(process.env)) { if (typeof value === 'string') { // Check if environment variable name suggests sensitive content if (sensitiveEnvVars.some(sensitive => key.toUpperCase().includes(sensitive))) { if (this.isWeakCredential(value)) { securityIssues.push({ field: `env.${key}`, issue: 'Weak credential in environment variable', recommendation: 'Use a strong credential and secure environment variable handling' }); } if (this.isDefaultCredential(value)) { securityIssues.push({ field: `env.${key}`, issue: 'Default credential in environment variable', recommendation: 'Replace with a secure, unique credential' }); } } } } return securityIssues; } getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj); } setNestedValue(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, key) => { if (!current[key] || typeof current[key] !== 'object') { current[key] = {}; } return current[key]; }, obj); target[lastKey] = value; } findUnknownFields(config, schema, prefix = '') { const unknownFields = []; const knownFields = new Set(schema.map(field => prefix ? `${prefix}.${field.name}` : field.name)); if (typeof config === 'object' && config !== null && !Array.isArray(config)) { for (const key of Object.keys(config)) { const fullPath = prefix ? `${prefix}.${key}` : key; if (!knownFields.has(fullPath)) { unknownFields.push(fullPath); } // Recursively check nested objects if (typeof config[key] === 'object' && config[key] !== null && !Array.isArray(config[key])) { unknownFields.push(...this.findUnknownFields(config[key], schema, fullPath)); } } } return unknownFields; } generateConfigReport(validationResult, includeRecommendations = true) { const report = []; report.push('=== Configuration Validation Report ===\n'); if (validationResult.isValid) { report.push('✅ Configuration is valid\n'); } else { report.push('❌ Configuration validation failed\n'); } // Errors if (validationResult.errors.length > 0) { report.push('🔍 Issues Found:'); validationResult.errors.forEach(error => { const icon = error.severity === 'error' ? '❌' : error.severity === 'warning' ? 'âš ī¸' : 'â„šī¸'; report.push(` ${icon} ${error.field}: ${error.message}`); }); report.push(''); } // Security issues if (validationResult.securityIssues && validationResult.securityIssues.length > 0) { report.push('🔒 Security Concerns:'); validationResult.securityIssues.forEach(issue => { report.push(` âš ī¸ ${issue.field}: ${issue.issue}`); if (includeRecommendations) { report.push(` 💡 ${issue.recommendation}`); } }); report.push(''); } if (validationResult.isValid && validationResult.securityIssues?.length === 0) { report.push('🎉 No security issues detected\n'); } return report.join('\n'); } } export const configValidator = ConfigValidator.getInstance(); //# sourceMappingURL=validation.js.map