survey-mcp-server
Version:
Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management
266 lines • 11.6 kB
JavaScript
export { ConfigValidator, configValidator } from './validation.js';
export { configSchema, developmentDefaults, productionDefaults, testDefaults, getEnvironmentDefaults } from './schema.js';
import { configValidator } from './validation.js';
import { configSchema, getEnvironmentDefaults } from './schema.js';
import { config as originalConfig, reloadConfig as originalReloadConfig } from '../utils/config.js';
import { logger } from '../utils/logger.js';
import { ConfigurationError } from '../middleware/error-handling.js';
export class SecureConfigManager {
constructor() {
this.validatedConfig = null;
this.lastValidation = null;
this.securityIssuesCount = 0;
}
static getInstance() {
if (!SecureConfigManager.instance) {
SecureConfigManager.instance = new SecureConfigManager();
}
return SecureConfigManager.instance;
}
async validateCurrentConfig(options = {}) {
const { validateOnLoad = true, strictMode = true, logSecurityIssues = true, throwOnValidationError = true, environment = process.env.NODE_ENV || 'production' } = options;
try {
// Apply environment-specific defaults
const envDefaults = getEnvironmentDefaults(environment);
const configWithDefaults = this.mergeConfigWithDefaults(originalConfig, envDefaults);
// Validate configuration against schema
const validationOptions = {
strictMode,
allowMissingOptional: true,
validateCredentials: true,
sanitizeSecrets: true
};
const validationResult = configValidator.validateConfiguration(configWithDefaults, configSchema, validationOptions);
this.lastValidation = new Date();
this.securityIssuesCount = validationResult.securityIssues?.length || 0;
if (logSecurityIssues && validationResult.securityIssues?.length) {
const report = configValidator.generateConfigReport(validationResult, true);
logger.warn('Configuration security issues detected:');
logger.warn(report);
}
if (!validationResult.isValid) {
const errorReport = configValidator.generateConfigReport(validationResult, false);
logger.error('Configuration validation failed:');
logger.error(errorReport);
if (throwOnValidationError) {
throw new ConfigurationError('Configuration validation failed', {
errors: validationResult.errors,
securityIssues: validationResult.securityIssues
});
}
}
else {
this.validatedConfig = validationResult.sanitizedConfig;
logger.info('Configuration validation passed successfully');
if (validationResult.securityIssues?.length === 0) {
logger.info('No security issues detected in configuration');
}
}
}
catch (error) {
logger.error('Configuration validation error:', error);
if (throwOnValidationError) {
throw new ConfigurationError(`Configuration validation failed: ${error.message}`, { originalError: error.message });
}
}
}
getValidatedConfig() {
if (!this.validatedConfig) {
throw new ConfigurationError('Configuration has not been validated. Call validateCurrentConfig() first.');
}
return this.validatedConfig;
}
async reloadAndValidateConfig(options = {}) {
// Reload the original configuration
originalReloadConfig();
// Validate the new configuration
await this.validateCurrentConfig(options);
}
validateCliArguments(args) {
const errors = [];
const sanitizedArgs = {};
try {
// Validate common CLI arguments
const cliValidation = [
{
name: 'mongodbUri',
pattern: /^mongodb:\/\/.+/,
message: 'MongoDB URI must be a valid MongoDB connection string'
},
{
name: 'mongodbDbName',
pattern: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
message: 'Database name must contain only alphanumeric characters, hyphens, and underscores'
},
{
name: 'typesenseHost',
pattern: /^[a-zA-Z0-9.-]+$/,
message: 'Typesense host must be a valid hostname or IP address'
},
{
name: 'typesensePort',
pattern: /^\d+$/,
message: 'Typesense port must be a number'
},
{
name: 'typesenseProtocol',
pattern: /^(http|https)$/,
message: 'Typesense protocol must be http or https'
},
{
name: 'logLevel',
pattern: /^(error|warn|info|debug|verbose)$/,
message: 'Log level must be one of: error, warn, info, debug, verbose'
}
];
for (const [key, value] of Object.entries(args)) {
if (value === undefined || value === null) {
continue;
}
// Find validation rule for this argument
const validation = cliValidation.find(v => v.name === key);
if (validation && typeof value === 'string') {
if (!validation.pattern.test(value)) {
errors.push(`${key}: ${validation.message}`);
continue;
}
}
// Basic sanitization
if (typeof value === 'string') {
// Remove dangerous characters
let sanitized = value.trim();
// Check for path traversal
if (sanitized.includes('../') || sanitized.includes('..\\')) {
errors.push(`${key}: Path traversal attempts not allowed`);
continue;
}
// Check for script injection
if (sanitized.includes('<script') || sanitized.includes('javascript:')) {
errors.push(`${key}: Script content not allowed`);
continue;
}
sanitizedArgs[key] = sanitized;
}
else {
sanitizedArgs[key] = value;
}
}
return {
isValid: errors.length === 0,
errors,
sanitizedArgs
};
}
catch (error) {
return {
isValid: false,
errors: [`CLI argument validation error: ${error.message}`],
sanitizedArgs: {}
};
}
}
generateSecurityReport() {
const recommendations = [];
// Check for common security issues
if (!this.lastValidation) {
recommendations.push('Configuration has not been validated');
}
else {
const timeSinceValidation = Date.now() - this.lastValidation.getTime();
if (timeSinceValidation > 24 * 60 * 60 * 1000) { // 24 hours
recommendations.push('Configuration validation is outdated (>24 hours)');
}
}
if (this.securityIssuesCount > 0) {
recommendations.push(`${this.securityIssuesCount} security issues detected - review and fix`);
}
// Check environment
const environment = process.env.NODE_ENV || 'unknown';
if (environment === 'unknown') {
recommendations.push('NODE_ENV is not set - specify development, production, or test');
}
// Check for required security settings in production
if (environment === 'production') {
const productionChecks = [
{ env: 'SECURITY_ENABLE_INPUT_VALIDATION', default: 'true' },
{ env: 'SECURITY_ENABLE_SANITIZATION', default: 'true' },
{ env: 'PERFORMANCE_ENABLE_CIRCUIT_BREAKER', default: 'true' }
];
for (const check of productionChecks) {
if (process.env[check.env] !== 'true') {
recommendations.push(`Set ${check.env}=${check.default} for production`);
}
}
}
return {
validationStatus: !this.lastValidation ? 'not_validated' :
this.securityIssuesCount > 0 ? 'failed' : 'passed',
lastValidation: this.lastValidation,
securityIssuesCount: this.securityIssuesCount,
recommendations
};
}
mergeConfigWithDefaults(config, defaults) {
const merged = JSON.parse(JSON.stringify(config)); // Deep clone
for (const [path, defaultValue] of Object.entries(defaults)) {
const current = this.getNestedValue(merged, path);
if (current === undefined || current === null) {
this.setNestedValue(merged, path, defaultValue);
}
}
return merged;
}
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;
}
getConfigFieldInfo(fieldName) {
return configSchema.find(field => field.name === fieldName);
}
listSensitiveFields() {
return configSchema
.filter(field => field.sensitive)
.map(field => field.name);
}
maskSensitiveValues(config) {
const masked = JSON.parse(JSON.stringify(config));
const sensitiveFields = this.listSensitiveFields();
for (const fieldPath of sensitiveFields) {
const value = this.getNestedValue(masked, fieldPath);
if (typeof value === 'string' && value.length > 0) {
if (value.length > 8) {
this.setNestedValue(masked, fieldPath, value.substring(0, 4) + '*'.repeat(value.length - 8) + value.substring(value.length - 4));
}
else {
this.setNestedValue(masked, fieldPath, '*'.repeat(value.length));
}
}
}
return masked;
}
}
export const secureConfigManager = SecureConfigManager.getInstance();
// Initialize configuration validation on module load if in production
if (process.env.NODE_ENV === 'production') {
secureConfigManager.validateCurrentConfig({
validateOnLoad: true,
strictMode: true,
logSecurityIssues: true,
throwOnValidationError: false // Don't crash on startup in production
}).catch(error => {
logger.error('Failed to validate configuration on startup:', error);
});
}
//# sourceMappingURL=index.js.map