add project

This commit is contained in:
GuilhermeStrice
2025-07-09 19:24:12 +01:00
parent 8d7c2d4b04
commit 1108bf3ef6
17 changed files with 4752 additions and 0 deletions

143
Models/Configuration.cs Normal file
View File

@ -0,0 +1,143 @@
using System.Text.Json.Serialization;
namespace DatabaseSnapshotsService.Models
{
public class SnapshotConfiguration
{
[JsonPropertyName("connectionString")]
public string ConnectionString { get; set; } = "Server=localhost;Database=trading;Uid=root;Pwd=password;";
[JsonPropertyName("binlogReader")]
public BinlogReaderConfig BinlogReader { get; set; } = new();
[JsonPropertyName("snapshotStorage")]
public SnapshotStorageConfig SnapshotStorage { get; set; } = new();
[JsonPropertyName("eventStore")]
public EventStoreConfig EventStore { get; set; } = new();
[JsonPropertyName("security")]
public SecurityConfig Security { get; set; } = new();
}
public class SecurityConfig
{
[JsonPropertyName("encryption")]
public bool Encryption { get; set; } = false;
[JsonPropertyName("encryptionKey")]
public string? EncryptionKey { get; set; }
}
public class BinlogReaderConfig
{
[JsonPropertyName("host")]
public string Host { get; set; } = "localhost";
[JsonPropertyName("port")]
public int Port { get; set; } = 3306;
[JsonPropertyName("username")]
public string Username { get; set; } = "binlog_reader";
[JsonPropertyName("password")]
public string Password { get; set; } = "secure_password";
[JsonPropertyName("serverId")]
public int ServerId { get; set; } = 999;
[JsonPropertyName("startPosition")]
public long StartPosition { get; set; } = 4;
[JsonPropertyName("heartbeatInterval")]
public int HeartbeatInterval { get; set; } = 30;
}
public class SnapshotStorageConfig
{
[JsonPropertyName("path")]
public string Path { get; set; } = "./snapshots";
[JsonPropertyName("compression")]
public bool Compression { get; set; } = true;
[JsonPropertyName("retentionDays")]
public int RetentionDays { get; set; } = 30;
[JsonPropertyName("maxFileSize")]
public long MaxFileSize { get; set; } = 100 * 1024 * 1024; // 100MB
[JsonPropertyName("dumpOptimizations")]
public DumpOptimizationConfig DumpOptimizations { get; set; } = new();
}
public class DumpOptimizationConfig
{
[JsonPropertyName("singleTransaction")]
public bool SingleTransaction { get; set; } = true;
[JsonPropertyName("includeRoutines")]
public bool IncludeRoutines { get; set; } = true;
[JsonPropertyName("includeTriggers")]
public bool IncludeTriggers { get; set; } = true;
[JsonPropertyName("includeEvents")]
public bool IncludeEvents { get; set; } = true;
[JsonPropertyName("extendedInsert")]
public bool ExtendedInsert { get; set; } = true;
[JsonPropertyName("completeInsert")]
public bool CompleteInsert { get; set; } = true;
[JsonPropertyName("hexBlob")]
public bool HexBlob { get; set; } = true;
[JsonPropertyName("netBufferLength")]
public int NetBufferLength { get; set; } = 16384;
[JsonPropertyName("maxAllowedPacket")]
public string MaxAllowedPacket { get; set; } = "1G";
[JsonPropertyName("excludeTables")]
public List<string> ExcludeTables { get; set; } = new();
[JsonPropertyName("includeTables")]
public List<string> IncludeTables { get; set; } = new();
// New options
[JsonPropertyName("quick")]
public bool Quick { get; set; } = true;
[JsonPropertyName("orderByPrimary")]
public bool OrderByPrimary { get; set; } = true;
[JsonPropertyName("flushLogs")]
public bool FlushLogs { get; set; } = true;
[JsonPropertyName("masterData")]
public int MasterData { get; set; } = 2;
[JsonPropertyName("compact")]
public bool Compact { get; set; } = false;
[JsonPropertyName("noAutocommit")]
public bool NoAutocommit { get; set; } = false;
[JsonPropertyName("lockTables")]
public bool LockTables { get; set; } = false;
}
public class EventStoreConfig
{
[JsonPropertyName("path")]
public string Path { get; set; } = "./events";
[JsonPropertyName("maxFileSize")]
public long MaxFileSize { get; set; } = 50 * 1024 * 1024; // 50MB
[JsonPropertyName("retentionDays")]
public int RetentionDays { get; set; } = 90;
[JsonPropertyName("batchSize")]
public int BatchSize { get; set; } = 1000;
[JsonPropertyName("flushInterval")]
public int FlushInterval { get; set; } = 5; // seconds
}
}

View 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));
}
}
}

242
Models/DataModels.cs Normal file
View File

@ -0,0 +1,242 @@
using System.Text.Json.Serialization;
namespace DatabaseSnapshotsService.Models
{
public class SnapshotInfo
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("dataSize")]
public long DataSize { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("userId")]
public int? UserId { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("filePath")]
public string FilePath { get; set; } = string.Empty;
[JsonPropertyName("checksum")]
public string Checksum { get; set; } = string.Empty;
// Binlog fields for incremental snapshots
[JsonPropertyName("binlogFile")]
public string? BinlogFile { get; set; }
[JsonPropertyName("binlogPosition")]
public long? BinlogPosition { get; set; }
[JsonPropertyName("parentSnapshotId")]
public int? ParentSnapshotId { get; set; }
[JsonPropertyName("incrementalBinlogStartFile")]
public string? IncrementalBinlogStartFile { get; set; }
[JsonPropertyName("incrementalBinlogStartPosition")]
public long? IncrementalBinlogStartPosition { get; set; }
[JsonPropertyName("incrementalBinlogEndFile")]
public string? IncrementalBinlogEndFile { get; set; }
[JsonPropertyName("incrementalBinlogEndPosition")]
public long? IncrementalBinlogEndPosition { get; set; }
}
public class DatabaseEvent
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("table")]
public string Table { get; set; } = string.Empty;
[JsonPropertyName("operation")]
public string Operation { get; set; } = string.Empty;
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
[JsonPropertyName("binlogPosition")]
public long BinlogPosition { get; set; }
[JsonPropertyName("serverId")]
public int ServerId { get; set; }
[JsonPropertyName("checksum")]
public string Checksum { get; set; } = string.Empty;
}
public class RecoveryPoint
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("eventCount")]
public long EventCount { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("lastEventId")]
public long LastEventId { get; set; }
}
public class ServiceStatus
{
[JsonPropertyName("status")]
public string Status { get; set; } = "Unknown";
[JsonPropertyName("databaseConnected")]
public bool DatabaseConnected { get; set; }
[JsonPropertyName("binlogReaderStatus")]
public string BinlogReaderStatus { get; set; } = "Unknown";
[JsonPropertyName("lastEventProcessed")]
public long LastEventProcessed { get; set; }
[JsonPropertyName("totalEvents")]
public long TotalEvents { get; set; }
[JsonPropertyName("activeSnapshots")]
public int ActiveSnapshots { get; set; }
[JsonPropertyName("uptime")]
public TimeSpan Uptime { get; set; }
[JsonPropertyName("lastSnapshot")]
public DateTime? LastSnapshot { get; set; }
}
public class HealthStatus
{
[JsonPropertyName("isHealthy")]
public bool IsHealthy { get; set; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; set; }
[JsonPropertyName("checks")]
public Dictionary<string, bool> Checks { get; set; } = new();
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
public class RestorePreview
{
[JsonPropertyName("targetTimestamp")]
public long TargetTimestamp { get; set; }
[JsonPropertyName("eventCount")]
public long EventCount { get; set; }
[JsonPropertyName("affectedTables")]
public List<string> AffectedTables { get; set; } = new();
[JsonPropertyName("estimatedDuration")]
public TimeSpan EstimatedDuration { get; set; }
[JsonPropertyName("snapshotId")]
public int? SnapshotId { get; set; }
[JsonPropertyName("warnings")]
public List<string> Warnings { get; set; } = new();
}
public class SnapshotMetadata
{
[JsonPropertyName("version")]
public string Version { get; set; } = "1.0";
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("databaseVersion")]
public string DatabaseVersion { get; set; } = string.Empty;
[JsonPropertyName("tables")]
public List<TableInfo> Tables { get; set; } = new();
[JsonPropertyName("checksum")]
public string Checksum { get; set; } = string.Empty;
[JsonPropertyName("compression")]
public bool Compression { get; set; }
[JsonPropertyName("encryption")]
public bool Encryption { get; set; }
}
public class TableInfo
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("rowCount")]
public long RowCount { get; set; }
[JsonPropertyName("dataSize")]
public long DataSize { get; set; }
[JsonPropertyName("indexSize")]
public long IndexSize { get; set; }
[JsonPropertyName("checksum")]
public string Checksum { get; set; } = string.Empty;
}
public enum SnapshotType
{
Full,
Trading,
User,
Incremental
}
public enum EventOperation
{
Insert,
Update,
Delete,
Truncate
}
public enum SnapshotStatus
{
Creating,
Completed,
Failed,
Corrupted
}
}

438
Models/InputValidation.cs Normal file
View File

@ -0,0 +1,438 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace DatabaseSnapshotsService.Models
{
public static class InputValidation
{
// Validation patterns
private static readonly Regex ValidFileNamePattern = new(@"^[a-zA-Z0-9._-]+$", RegexOptions.Compiled);
private static readonly Regex ValidPathPattern = new(@"^[a-zA-Z0-9/._-]+$", RegexOptions.Compiled);
private static readonly Regex ValidNamePattern = new(@"^[a-zA-Z0-9\s._-]{1,100}$", RegexOptions.Compiled);
private static readonly Regex ValidDescriptionPattern = new(@"^[a-zA-Z0-9\s.,!?@#$%^&*()_+-=:;'""<>/\\|`~]{0,500}$", RegexOptions.Compiled);
private static readonly Regex ValidTableNamePattern = new(@"^[a-zA-Z][a-zA-Z0-9_]*$", RegexOptions.Compiled);
private static readonly Regex ValidOperationPattern = new(@"^(insert|update|delete|truncate|query)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ValidEventTypePattern = new(@"^(binlog|status|error|info)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Dangerous patterns to detect
private static readonly Regex[] DangerousPatterns = {
new(@"\.\./", RegexOptions.Compiled), // Path traversal
new(@"\.\.\\", RegexOptions.Compiled), // Windows path traversal
new(@"[<>""'&]", RegexOptions.Compiled), // HTML/XML injection
new(@"(union|select|insert|update|delete|drop|create|alter|exec|execute|script|javascript|vbscript|onload|onerror)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // SQL/script injection
new(@"(\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08|\x0B|\x0C|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F)", RegexOptions.Compiled), // Control characters
};
public static class SnapshotValidation
{
public static ValidationResult ValidateSnapshotName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return new ValidationResult("Snapshot name cannot be empty");
}
if (name.Length > 100)
{
return new ValidationResult("Snapshot name cannot exceed 100 characters");
}
if (!ValidNamePattern.IsMatch(name))
{
return new ValidationResult("Snapshot name contains invalid characters. Use only letters, numbers, spaces, dots, underscores, and hyphens");
}
if (ContainsDangerousPatterns(name))
{
return new ValidationResult("Snapshot name contains potentially dangerous content");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateSnapshotDescription(string? description)
{
if (string.IsNullOrWhiteSpace(description))
{
return ValidationResult.Success!; // Description is optional
}
if (description.Length > 500)
{
return new ValidationResult("Snapshot description cannot exceed 500 characters");
}
if (!ValidDescriptionPattern.IsMatch(description))
{
return new ValidationResult("Snapshot description contains invalid characters");
}
if (ContainsDangerousPatterns(description))
{
return new ValidationResult("Snapshot description contains potentially dangerous content");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateSnapshotId(int id)
{
if (id <= 0)
{
return new ValidationResult("Snapshot ID must be a positive integer");
}
if (id > int.MaxValue)
{
return new ValidationResult("Snapshot ID is too large");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateSnapshotType(string type)
{
if (string.IsNullOrWhiteSpace(type))
{
return new ValidationResult("Snapshot type cannot be empty");
}
var validTypes = new[] { "full", "incremental", "trading", "user" };
if (!validTypes.Contains(type.ToLowerInvariant()))
{
return new ValidationResult($"Invalid snapshot type. Must be one of: {string.Join(", ", validTypes)}");
}
return ValidationResult.Success!;
}
}
public static class EventValidation
{
public static ValidationResult ValidateTableName(string tableName)
{
if (string.IsNullOrWhiteSpace(tableName))
{
return new ValidationResult("Table name cannot be empty");
}
if (tableName.Length > 64)
{
return new ValidationResult("Table name cannot exceed 64 characters");
}
if (!ValidTableNamePattern.IsMatch(tableName))
{
return new ValidationResult("Table name contains invalid characters. Use only letters, numbers, and underscores, starting with a letter");
}
if (ContainsDangerousPatterns(tableName))
{
return new ValidationResult("Table name contains potentially dangerous content");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateOperation(string operation)
{
if (string.IsNullOrWhiteSpace(operation))
{
return new ValidationResult("Operation cannot be empty");
}
if (!ValidOperationPattern.IsMatch(operation))
{
return new ValidationResult("Invalid operation. Must be one of: insert, update, delete, truncate, query");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateEventType(string eventType)
{
if (string.IsNullOrWhiteSpace(eventType))
{
return new ValidationResult("Event type cannot be empty");
}
if (!ValidEventTypePattern.IsMatch(eventType))
{
return new ValidationResult("Invalid event type. Must be one of: binlog, status, error, info");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateEventData(string data)
{
if (string.IsNullOrWhiteSpace(data))
{
return new ValidationResult("Event data cannot be empty");
}
if (data.Length > 10000) // 10KB limit
{
return new ValidationResult("Event data cannot exceed 10KB");
}
if (ContainsDangerousPatterns(data))
{
return new ValidationResult("Event data contains potentially dangerous content");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateLimit(int limit)
{
if (limit <= 0)
{
return new ValidationResult("Limit must be a positive integer");
}
if (limit > 10000)
{
return new ValidationResult("Limit cannot exceed 10000");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateTimestamp(long timestamp)
{
if (timestamp < 0)
{
return new ValidationResult("Timestamp cannot be negative");
}
var maxTimestamp = DateTimeOffset.MaxValue.ToUnixTimeSeconds();
if (timestamp > maxTimestamp)
{
return new ValidationResult("Timestamp is too far in the future");
}
return ValidationResult.Success!;
}
}
public static class RecoveryValidation
{
public static ValidationResult ValidateRecoveryPointName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return new ValidationResult("Recovery point name cannot be empty");
}
if (name.Length > 100)
{
return new ValidationResult("Recovery point name cannot exceed 100 characters");
}
if (!ValidNamePattern.IsMatch(name))
{
return new ValidationResult("Recovery point name contains invalid characters. Use only letters, numbers, spaces, dots, underscores, and hyphens");
}
if (ContainsDangerousPatterns(name))
{
return new ValidationResult("Recovery point name contains potentially dangerous content");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateRecoveryPointDescription(string? description)
{
if (string.IsNullOrWhiteSpace(description))
{
return ValidationResult.Success!; // Description is optional
}
if (description.Length > 500)
{
return new ValidationResult("Recovery point description cannot exceed 500 characters");
}
if (!ValidDescriptionPattern.IsMatch(description))
{
return new ValidationResult("Recovery point description contains invalid characters");
}
if (ContainsDangerousPatterns(description))
{
return new ValidationResult("Recovery point description contains potentially dangerous content");
}
return ValidationResult.Success!;
}
}
public static class FileValidation
{
public static ValidationResult ValidateFilePath(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
return new ValidationResult("File path cannot be empty");
}
if (filePath.Length > 260) // Windows path limit
{
return new ValidationResult("File path cannot exceed 260 characters");
}
if (!ValidPathPattern.IsMatch(filePath))
{
return new ValidationResult("File path contains invalid characters");
}
if (ContainsDangerousPatterns(filePath))
{
return new ValidationResult("File path contains potentially dangerous content");
}
// Check for path traversal attempts
var normalizedPath = Path.GetFullPath(filePath);
if (normalizedPath.Contains("..") || normalizedPath.Contains("~"))
{
return new ValidationResult("File path contains invalid path traversal characters");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return new ValidationResult("File name cannot be empty");
}
if (fileName.Length > 255)
{
return new ValidationResult("File name cannot exceed 255 characters");
}
if (!ValidFileNamePattern.IsMatch(fileName))
{
return new ValidationResult("File name contains invalid characters. Use only letters, numbers, dots, underscores, and hyphens");
}
if (ContainsDangerousPatterns(fileName))
{
return new ValidationResult("File name contains potentially dangerous content");
}
// Check for reserved Windows filenames
var reservedNames = new[]
{
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
};
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant();
if (reservedNames.Contains(fileNameWithoutExtension))
{
return new ValidationResult("File name is a reserved system name");
}
return ValidationResult.Success!;
}
}
public static class DataValidation
{
public static ValidationResult ValidateDataContains(string dataContains)
{
if (string.IsNullOrWhiteSpace(dataContains))
{
return new ValidationResult("Data contains filter cannot be empty");
}
if (dataContains.Length > 1000)
{
return new ValidationResult("Data contains filter cannot exceed 1000 characters");
}
if (ContainsDangerousPatterns(dataContains))
{
return new ValidationResult("Data contains filter contains potentially dangerous content");
}
return ValidationResult.Success!;
}
public static ValidationResult ValidateOutputFile(string outputFile)
{
if (string.IsNullOrWhiteSpace(outputFile))
{
return new ValidationResult("Output file path cannot be empty");
}
return FileValidation.ValidateFilePath(outputFile);
}
}
private static bool ContainsDangerousPatterns(string input)
{
return DangerousPatterns.Any(pattern => pattern.IsMatch(input));
}
public static string SanitizeString(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return string.Empty;
}
// Remove control characters
var sanitized = Regex.Replace(input, @"[\x00-\x1F\x7F]", "");
// Trim whitespace
sanitized = sanitized.Trim();
return sanitized;
}
public static string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return string.Empty;
}
// Remove invalid characters for file names
var sanitized = Regex.Replace(fileName, @"[<>:""/\\|?*\x00-\x1F]", "");
// Trim and limit length
sanitized = sanitized.Trim();
if (sanitized.Length > 255)
{
sanitized = sanitized.Substring(0, 255);
}
return sanitized;
}
public static string SanitizePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
// Remove path traversal attempts
var sanitized = path.Replace("..", "").Replace("~", "");
// Remove invalid characters
sanitized = Regex.Replace(sanitized, @"[<>""|?\x00-\x1F]", "");
// Normalize path separators
sanitized = sanitized.Replace('\\', '/');
return sanitized;
}
}
}