Files
DatabaseSnapshots/Models/ConfigurationValidation.cs
GuilhermeStrice 1108bf3ef6 add project
2025-07-09 19:24:12 +01:00

252 lines
10 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace DatabaseSnapshotsService.Models
{
public static class ConfigurationValidation
{
public static List<ValidationResult> ValidateConfiguration(SnapshotConfiguration config)
{
var errors = new List<ValidationResult>();
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<ValidationResult> 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<ValidationResult> 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<ValidationResult> 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<ValidationResult> 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<ValidationResult> 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<ValidationResult> 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));
}
}
}