using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; namespace MarketAlly.AIPlugin.Analysis.Infrastructure { /// /// Input validation and security service for analysis operations /// public class InputValidator { private readonly ILogger? _logger; private static readonly HashSet AllowedFileExtensions = new(StringComparer.OrdinalIgnoreCase) { ".cs", ".csproj", ".sln", ".json", ".xml", ".config", ".md", ".txt", ".dll", ".exe", ".pdb", ".nuspec", ".props", ".targets" }; private static readonly Regex SafePathRegex = new(@"^[a-zA-Z0-9\\\/:._\-\s]+$", RegexOptions.Compiled); private static readonly Regex DangerousPatternRegex = new(@"(\.\.[\\/]|? logger = null) { _logger = logger; } /// /// Validates and sanitizes a file path /// public ValidationResult ValidateFilePath(string? filePath) { if (string.IsNullOrWhiteSpace(filePath)) { return ValidationResult.Failure("File path cannot be null or empty"); } // Check for dangerous patterns if (DangerousPatternRegex.IsMatch(filePath)) { _logger?.LogWarning("Dangerous pattern detected in file path: {FilePath}", filePath); return ValidationResult.Failure("File path contains potentially dangerous patterns"); } // Validate path format if (!SafePathRegex.IsMatch(filePath)) { return ValidationResult.Failure("File path contains invalid characters"); } // Check for path traversal attempts var normalizedPath = Path.GetFullPath(filePath); if (filePath.Contains("..") && !normalizedPath.StartsWith(Path.GetFullPath("."))) { _logger?.LogWarning("Path traversal attempt detected: {FilePath}", filePath); return ValidationResult.Failure("Path traversal detected"); } // Validate file extension var extension = Path.GetExtension(filePath); if (!string.IsNullOrEmpty(extension) && !AllowedFileExtensions.Contains(extension)) { return ValidationResult.Failure($"File extension '{extension}' is not allowed"); } return ValidationResult.Success(normalizedPath); } /// /// Validates plugin parameters for security issues /// public ValidationResult ValidatePluginParameters(Dictionary? parameters) { if (parameters == null) { return ValidationResult.Success(); } foreach (var kvp in parameters) { // Validate parameter name if (string.IsNullOrWhiteSpace(kvp.Key) || !IsValidParameterName(kvp.Key)) { return ValidationResult.Failure($"Invalid parameter name: {kvp.Key}"); } // Validate parameter value var valueValidation = ValidateParameterValue(kvp.Key, kvp.Value); if (!valueValidation.IsValid) { return valueValidation; } } return ValidationResult.Success(); } /// /// Validates analysis configuration settings /// public ValidationResult ValidateConfiguration(AnalysisConfiguration? config) { if (config == null) { return ValidationResult.Failure("Configuration cannot be null"); } // Validate timeout values if (config.DefaultTimeout <= TimeSpan.Zero || config.DefaultTimeout > TimeSpan.FromHours(1)) { return ValidationResult.Failure("Default timeout must be between 1 second and 1 hour"); } // Validate concurrency limits if (config.MaxConcurrentAnalyses < 1 || config.MaxConcurrentAnalyses > Environment.ProcessorCount * 4) { return ValidationResult.Failure($"Max concurrent analyses must be between 1 and {Environment.ProcessorCount * 4}"); } // Validate cache settings if (config.CacheExpirationTime < TimeSpan.FromMinutes(1) || config.CacheExpirationTime > TimeSpan.FromDays(7)) { return ValidationResult.Failure("Cache expiration time must be between 1 minute and 7 days"); } // Validate security settings if (config.AllowDynamicPluginLoading && string.IsNullOrWhiteSpace(config.TrustedPluginDirectory)) { return ValidationResult.Failure("Trusted plugin directory must be specified when dynamic plugin loading is enabled"); } return ValidationResult.Success(); } /// /// Sanitizes string input to remove potentially dangerous content /// public string SanitizeInput(string? input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } // Remove or escape potentially dangerous characters var sanitized = input .Replace("<", "<") .Replace(">", ">") .Replace("&", "&") .Replace("\"", """) .Replace("'", "'") .Replace("/", "/"); // Remove null bytes and control characters (except common whitespace) sanitized = Regex.Replace(sanitized, @"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", ""); return sanitized.Trim(); } /// /// Validates that a directory path is safe and accessible /// public ValidationResult ValidateDirectoryPath(string? directoryPath) { if (string.IsNullOrWhiteSpace(directoryPath)) { return ValidationResult.Failure("Directory path cannot be null or empty"); } var pathValidation = ValidateFilePath(directoryPath); if (!pathValidation.IsValid) { return pathValidation; } try { var fullPath = Path.GetFullPath(directoryPath); // Check if directory exists and is accessible if (!Directory.Exists(fullPath)) { return ValidationResult.Failure("Directory does not exist"); } // Basic permission check try { Directory.GetFiles(fullPath, "*", SearchOption.TopDirectoryOnly); } catch (UnauthorizedAccessException) { _logger?.LogWarning("Access denied to directory: {DirectoryPath}", fullPath); return ValidationResult.Failure("Access denied to directory"); } return ValidationResult.Success(fullPath); } catch (Exception ex) { _logger?.LogError(ex, "Error validating directory path: {DirectoryPath}", directoryPath); return ValidationResult.Failure("Invalid directory path"); } } private bool IsValidParameterName(string parameterName) { // Parameter names should only contain alphanumeric characters, underscores, and dots return Regex.IsMatch(parameterName, @"^[a-zA-Z0-9_\.]+$"); } private ValidationResult ValidateParameterValue(string parameterName, object? value) { if (value == null) { return ValidationResult.Success(); } // Check for potentially dangerous string values if (value is string stringValue) { if (DangerousPatternRegex.IsMatch(stringValue)) { _logger?.LogWarning("Dangerous pattern detected in parameter {ParameterName}: {Value}", parameterName, stringValue); return ValidationResult.Failure($"Parameter '{parameterName}' contains potentially dangerous content"); } // Check string length limits if (stringValue.Length > 10000) { return ValidationResult.Failure($"Parameter '{parameterName}' exceeds maximum length (10000 characters)"); } } // Validate file paths in parameters if (parameterName.EndsWith("Path", StringComparison.OrdinalIgnoreCase) && value is string pathValue) { var pathValidation = ValidateFilePath(pathValue); if (!pathValidation.IsValid) { return ValidationResult.Failure($"Invalid file path in parameter '{parameterName}': {pathValidation.ErrorMessage}"); } } return ValidationResult.Success(); } } /// /// Result of input validation operation /// public class ValidationResult { public bool IsValid { get; private set; } public string? ErrorMessage { get; private set; } public string? SanitizedValue { get; private set; } private ValidationResult(bool isValid, string? errorMessage = null, string? sanitizedValue = null) { IsValid = isValid; ErrorMessage = errorMessage; SanitizedValue = sanitizedValue; } public static ValidationResult Success(string? sanitizedValue = null) { return new ValidationResult(true, sanitizedValue: sanitizedValue); } public static ValidationResult Failure(string errorMessage) { return new ValidationResult(false, errorMessage); } } }