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