using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; namespace DatabaseSnapshotsService.Models { public static class ConfigurationValidation { public static List ValidateConfiguration(SnapshotConfiguration config) { var errors = new List(); if (config == null) { errors.Add(new ValidationResult("Configuration cannot be null")); return errors; } ValidateConnectionString(config.ConnectionString, errors); ValidateBinlogReader(config.BinlogReader, errors); ValidateSnapshotStorage(config.SnapshotStorage, errors); ValidateEventStore(config.EventStore, errors); ValidateSecurity(config.Security, errors); return errors; } private static void ValidateConnectionString(string connectionString, List errors) { if (string.IsNullOrWhiteSpace(connectionString)) { errors.Add(new ValidationResult("Connection string cannot be empty")); return; } // Basic connection string validation var requiredParams = new[] { "Server", "Database", "Uid", "Pwd" }; foreach (var param in requiredParams) { if (!connectionString.Contains($"{param}=")) { errors.Add(new ValidationResult($"Connection string must contain {param} parameter")); } } // Check for potentially dangerous patterns var dangerousPatterns = new[] { @"--.*", // SQL comments @";\s*DROP\s+", // DROP statements @";\s*DELETE\s+", // DELETE statements @";\s*TRUNCATE\s+", // TRUNCATE statements @";\s*ALTER\s+", // ALTER statements @";\s*CREATE\s+", // CREATE statements }; foreach (var pattern in dangerousPatterns) { if (Regex.IsMatch(connectionString, pattern, RegexOptions.IgnoreCase)) { errors.Add(new ValidationResult($"Connection string contains potentially dangerous SQL pattern: {pattern}")); } } } private static void ValidateBinlogReader(BinlogReaderConfig config, List errors) { if (string.IsNullOrWhiteSpace(config.Host)) { errors.Add(new ValidationResult("Binlog reader host cannot be empty")); } if (config.Port < 1 || config.Port > 65535) { errors.Add(new ValidationResult("Binlog reader port must be between 1 and 65535")); } if (string.IsNullOrWhiteSpace(config.Username)) { errors.Add(new ValidationResult("Binlog reader username cannot be empty")); } if (string.IsNullOrWhiteSpace(config.Password)) { errors.Add(new ValidationResult("Binlog reader password cannot be empty")); } if (config.ServerId < 1 || config.ServerId > 4294967295) { errors.Add(new ValidationResult("Binlog reader server ID must be between 1 and 4294967295")); } if (config.StartPosition < 4) { errors.Add(new ValidationResult("Binlog reader start position must be at least 4")); } if (config.HeartbeatInterval < 1 || config.HeartbeatInterval > 3600) { errors.Add(new ValidationResult("Binlog reader heartbeat interval must be between 1 and 3600 seconds")); } } private static void ValidateSnapshotStorage(SnapshotStorageConfig config, List errors) { if (string.IsNullOrWhiteSpace(config.Path)) { errors.Add(new ValidationResult("Snapshot storage path cannot be empty")); } else { // Check for path traversal attempts var normalizedPath = Path.GetFullPath(config.Path); if (normalizedPath.Contains("..") || normalizedPath.Contains("~")) { errors.Add(new ValidationResult("Snapshot storage path contains invalid characters")); } } if (config.RetentionDays < 1 || config.RetentionDays > 3650) // Max 10 years { errors.Add(new ValidationResult("Snapshot retention days must be between 1 and 3650")); } if (config.MaxFileSize < 1024 * 1024 || config.MaxFileSize > 10L * 1024 * 1024 * 1024) // 1MB to 10GB { errors.Add(new ValidationResult("Snapshot max file size must be between 1MB and 10GB")); } // Validate dump optimizations ValidateDumpOptimizations(config.DumpOptimizations, errors); } private static void ValidateDumpOptimizations(DumpOptimizationConfig config, List errors) { if (config.NetBufferLength < 1024 || config.NetBufferLength > 1048576) // 1KB to 1MB { errors.Add(new ValidationResult("Net buffer length must be between 1024 and 1048576 bytes")); } // Validate max allowed packet format (e.g., "1G", "512M", "1024K") if (!string.IsNullOrWhiteSpace(config.MaxAllowedPacket)) { var packetPattern = @"^\d+[KMGT]?$"; if (!Regex.IsMatch(config.MaxAllowedPacket, packetPattern)) { errors.Add(new ValidationResult("Max allowed packet must be in format: number[K|M|G|T] (e.g., '1G', '512M')")); } } // Validate table names in exclude/include lists foreach (var table in config.ExcludeTables.Concat(config.IncludeTables)) { if (string.IsNullOrWhiteSpace(table)) { errors.Add(new ValidationResult("Table names cannot be empty")); } else if (!Regex.IsMatch(table, @"^[a-zA-Z_][a-zA-Z0-9_]*$")) { errors.Add(new ValidationResult($"Invalid table name format: {table}")); } } // Check for conflicts between include and exclude tables var conflicts = config.IncludeTables.Intersect(config.ExcludeTables, StringComparer.OrdinalIgnoreCase); if (conflicts.Any()) { errors.Add(new ValidationResult($"Table(s) cannot be both included and excluded: {string.Join(", ", conflicts)}")); } } private static void ValidateEventStore(EventStoreConfig config, List errors) { if (string.IsNullOrWhiteSpace(config.Path)) { errors.Add(new ValidationResult("Event store path cannot be empty")); } else { // Check for path traversal attempts var normalizedPath = Path.GetFullPath(config.Path); if (normalizedPath.Contains("..") || normalizedPath.Contains("~")) { errors.Add(new ValidationResult("Event store path contains invalid characters")); } } if (config.MaxFileSize < 1024 * 1024 || config.MaxFileSize > 5L * 1024 * 1024 * 1024) // 1MB to 5GB { errors.Add(new ValidationResult("Event store max file size must be between 1MB and 5GB")); } if (config.RetentionDays < 1 || config.RetentionDays > 3650) // Max 10 years { errors.Add(new ValidationResult("Event store retention days must be between 1 and 3650")); } if (config.BatchSize < 1 || config.BatchSize > 10000) { errors.Add(new ValidationResult("Event store batch size must be between 1 and 10000")); } if (config.FlushInterval < 1 || config.FlushInterval > 300) // 1 second to 5 minutes { errors.Add(new ValidationResult("Event store flush interval must be between 1 and 300 seconds")); } } private static void ValidateSecurity(SecurityConfig config, List errors) { if (config.Encryption && string.IsNullOrWhiteSpace(config.EncryptionKey)) { errors.Add(new ValidationResult("Encryption key is required when encryption is enabled")); } if (!string.IsNullOrWhiteSpace(config.EncryptionKey)) { if (config.EncryptionKey.Length < 32) { errors.Add(new ValidationResult("Encryption key must be at least 32 characters long")); } // Check for weak encryption keys if (IsWeakEncryptionKey(config.EncryptionKey)) { errors.Add(new ValidationResult("Encryption key is too weak. Use a stronger key with mixed characters")); } } } private static bool IsWeakEncryptionKey(string key) { // Check for common weak patterns if (key.Length < 32) return true; // Check for repeated characters if (Regex.IsMatch(key, @"(.)\1{10,}")) return true; // Check for sequential characters if (Regex.IsMatch(key, @"(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)", RegexOptions.IgnoreCase)) return true; // Check for common weak keys var weakKeys = new[] { "password", "123456", "qwerty", "admin", "root", "secret", "key", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "12345678901234567890123456789012" }; return weakKeys.Any(wk => key.Equals(wk, StringComparison.OrdinalIgnoreCase)); } } }