MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Learning/Services/SecurityService.cs

344 lines
13 KiB
C#
Executable File

using MarketAlly.AIPlugin.Learning.Configuration;
using MarketAlly.AIPlugin.Learning.Exceptions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace MarketAlly.AIPlugin.Learning.Services
{
/// <summary>
/// Service for handling security validation and sanitization
/// </summary>
public interface ISecurityService
{
bool IsPathSafe(string path);
bool IsFileAllowed(string filePath);
string SanitizeInput(string input);
bool ValidateFileSize(string filePath);
bool ValidateDirectoryAccess(string directoryPath);
string GenerateSecureSessionId();
ValidationResult ValidateConfiguration(LearningConfiguration configuration);
bool IsDirectoryWithinBounds(string directory);
bool IsOperationAllowed(string operation, SessionContext? context);
}
public class SecurityService : ISecurityService
{
private readonly SecurityConfiguration _config;
private readonly ILogger<SecurityService> _logger;
private readonly string _workingDirectory;
private readonly Regex _unsafeCharactersRegex;
public SecurityService(IOptions<LearningConfiguration> options, ILogger<SecurityService> logger, string? workingDirectory = null)
{
_config = options.Value.Security;
_logger = logger;
_workingDirectory = workingDirectory ?? Environment.CurrentDirectory;
_unsafeCharactersRegex = new Regex(@"[<>""|?*\x00-\x1f]", RegexOptions.Compiled);
}
public bool IsPathSafe(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
_logger.LogWarning("Path validation failed: null or empty path");
return false;
}
try
{
if (!_config.EnablePathValidation)
return true;
// Get the full path to resolve any relative path components
var fullPath = Path.GetFullPath(path);
// Ensure the path is within the working directory
if (!fullPath.StartsWith(_workingDirectory, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Path validation failed: path {Path} is outside working directory {WorkingDirectory}",
fullPath, _workingDirectory);
return false;
}
// Check for forbidden directories
var pathParts = fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
foreach (var part in pathParts)
{
if (_config.ForbiddenDirectories.Contains(part, StringComparer.OrdinalIgnoreCase))
{
_logger.LogWarning("Path validation failed: path {Path} contains forbidden directory {ForbiddenDir}",
fullPath, part);
return false;
}
}
// Check for unsafe characters
if (_unsafeCharactersRegex.IsMatch(fullPath))
{
_logger.LogWarning("Path validation failed: path {Path} contains unsafe characters", fullPath);
return false;
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during path validation for {Path}", path);
return false;
}
}
public bool IsFileAllowed(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
return false;
try
{
// Check path safety first
if (!IsPathSafe(filePath))
return false;
// Check file extension
var extension = Path.GetExtension(filePath);
if (!_config.AllowedFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
_logger.LogWarning("File rejected: extension {Extension} not in allowed list for {FilePath}",
extension, filePath);
return false;
}
// Check if file exists and validate size
if (File.Exists(filePath) && !ValidateFileSize(filePath))
{
return false;
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during file validation for {FilePath}", filePath);
return false;
}
}
public string SanitizeInput(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
if (!_config.EnableInputSanitization)
return input;
try
{
// Remove or replace unsafe characters
var sanitized = _unsafeCharactersRegex.Replace(input, "_");
// Trim whitespace
sanitized = sanitized.Trim();
// Limit length to prevent buffer overflow attacks
const int maxLength = 1000;
if (sanitized.Length > maxLength)
{
sanitized = sanitized.Substring(0, maxLength);
_logger.LogWarning("Input truncated from {OriginalLength} to {MaxLength} characters",
input.Length, maxLength);
}
return sanitized;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during input sanitization");
return string.Empty;
}
}
public bool ValidateFileSize(string filePath)
{
try
{
if (!File.Exists(filePath))
return true; // Non-existent files are considered valid for size check
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > _config.MaxFileSizeBytes)
{
_logger.LogWarning("File {FilePath} exceeds maximum size limit {MaxSize} bytes (actual: {ActualSize})",
filePath, _config.MaxFileSizeBytes, fileInfo.Length);
return false;
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during file size validation for {FilePath}", filePath);
return false;
}
}
public bool ValidateDirectoryAccess(string directoryPath)
{
try
{
if (!IsPathSafe(directoryPath))
return false;
// Check if directory exists and is accessible
if (Directory.Exists(directoryPath))
{
// Try to enumerate files to test read access
_ = Directory.EnumerateFiles(directoryPath).Take(1).ToList();
}
return true;
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Access denied to directory {DirectoryPath}", directoryPath);
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during directory access validation for {DirectoryPath}", directoryPath);
return false;
}
}
public string GenerateSecureSessionId()
{
try
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[16];
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes).Replace("+", "-").Replace("/", "_").TrimEnd('=');
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during secure session ID generation");
return Guid.NewGuid().ToString("N")[..16]; // Fallback to GUID
}
}
public ValidationResult ValidateConfiguration(LearningConfiguration configuration)
{
var errors = new List<string>();
try
{
// Validate Git configuration
if (string.IsNullOrWhiteSpace(configuration.Git.BranchPrefix))
errors.Add("Git.BranchPrefix cannot be empty");
if (string.IsNullOrWhiteSpace(configuration.Git.CommitterName))
errors.Add("Git.CommitterName cannot be empty");
if (string.IsNullOrWhiteSpace(configuration.Git.CommitterEmail))
errors.Add("Git.CommitterEmail cannot be empty");
else if (!IsValidEmail(configuration.Git.CommitterEmail))
errors.Add("Git.CommitterEmail must be a valid email address");
// Validate Security configuration
if (configuration.Security.AllowedFileExtensions?.Length == 0)
errors.Add("Security.AllowedFileExtensions cannot be empty");
if (configuration.Security.MaxFileSizeBytes <= 0)
errors.Add("Security.MaxFileSizeBytes must be positive");
// Validate AI configuration
if (configuration.AI.MaxContextTokens <= 0)
errors.Add("AI.MaxContextTokens must be positive");
if (configuration.AI.MaxSearchResults <= 0)
errors.Add("AI.MaxSearchResults must be positive");
// Validate Learning Mode configurations
ValidateLearningModeSettings("Conservative", configuration.LearningModes.Conservative, errors);
ValidateLearningModeSettings("Moderate", configuration.LearningModes.Moderate, errors);
ValidateLearningModeSettings("Aggressive", configuration.LearningModes.Aggressive, errors);
return errors.Count == 0 ? ValidationResult.Success() : ValidationResult.Failure(errors);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception during configuration validation");
return ValidationResult.Failure($"Configuration validation failed: {ex.Message}");
}
}
public bool IsDirectoryWithinBounds(string directory)
{
if (string.IsNullOrWhiteSpace(directory))
return false;
try
{
var fullPath = Path.GetFullPath(directory);
var workingPath = Path.GetFullPath(_workingDirectory);
return fullPath.StartsWith(workingPath, StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to validate directory bounds for {Directory}", directory);
return false;
}
}
public bool IsOperationAllowed(string operation, SessionContext? context)
{
if (string.IsNullOrWhiteSpace(operation))
return false;
if (context == null)
return false;
// Define allowed operations
var allowedOperations = new[] { "read", "write", "analyze", "refactor", "validate" };
var dangerousOperations = new[] { "execute", "delete", "install", "uninstall" };
if (dangerousOperations.Contains(operation.ToLower()))
return false;
return allowedOperations.Contains(operation.ToLower());
}
private bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
private void ValidateLearningModeSettings(string modeName, LearningModeSettings settings, List<string> errors)
{
if (settings.MaxIterations <= 0)
errors.Add($"{modeName}.MaxIterations must be positive");
if (settings.MaxAttemptsPerFile <= 0)
errors.Add($"{modeName}.MaxAttemptsPerFile must be positive");
if (settings.TimeoutMinutes <= 0)
errors.Add($"{modeName}.TimeoutMinutes must be positive");
if (settings.AllowedApproaches?.Length == 0)
errors.Add($"{modeName}.AllowedApproaches cannot be empty");
if (settings.RiskThreshold < 0 || settings.RiskThreshold > 1)
errors.Add($"{modeName}.RiskThreshold must be between 0 and 1");
}
}
}