MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Analysis/Infrastructure/InputValidator.cs

279 lines
10 KiB
C#
Executable File

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
{
/// <summary>
/// Input validation and security service for analysis operations
/// </summary>
public class InputValidator
{
private readonly ILogger<InputValidator>? _logger;
private static readonly HashSet<string> 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(@"(\.\.[\\/]|<script|javascript:|vbscript:|onload=|onerror=)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public InputValidator(ILogger<InputValidator>? logger = null)
{
_logger = logger;
}
/// <summary>
/// Validates and sanitizes a file path
/// </summary>
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);
}
/// <summary>
/// Validates plugin parameters for security issues
/// </summary>
public ValidationResult ValidatePluginParameters(Dictionary<string, object>? 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();
}
/// <summary>
/// Validates analysis configuration settings
/// </summary>
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();
}
/// <summary>
/// Sanitizes string input to remove potentially dangerous content
/// </summary>
public string SanitizeInput(string? input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
// Remove or escape potentially dangerous characters
var sanitized = input
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace("&", "&amp;")
.Replace("\"", "&quot;")
.Replace("'", "&#x27;")
.Replace("/", "&#x2F;");
// Remove null bytes and control characters (except common whitespace)
sanitized = Regex.Replace(sanitized, @"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "");
return sanitized.Trim();
}
/// <summary>
/// Validates that a directory path is safe and accessible
/// </summary>
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();
}
}
/// <summary>
/// Result of input validation operation
/// </summary>
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);
}
}
}