add project
This commit is contained in:
252
Models/ConfigurationValidation.cs
Normal file
252
Models/ConfigurationValidation.cs
Normal file
@ -0,0 +1,252 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user