344 lines
13 KiB
C#
Executable File
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");
|
|
}
|
|
}
|
|
} |