using Microsoft.Extensions.Configuration; using StellaOps.Doctor.Models; using StellaOps.Doctor.Plugins; using System.Globalization; namespace StellaOps.Doctor.Plugins.Security.Checks; /// /// Validates rate limiting configuration. /// public sealed class RateLimitingCheck : IDoctorCheck { /// public string CheckId => "check.security.ratelimit"; /// public string Name => "Rate Limiting"; /// public string Description => "Validates rate limiting is configured to prevent abuse"; /// public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; /// public IReadOnlyList Tags => ["security", "ratelimit", "api"]; /// public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50); /// public bool CanRun(DoctorPluginContext context) => true; /// public Task RunAsync(DoctorPluginContext context, CancellationToken ct) { var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString()); var rateLimitEnabled = context.Configuration.GetValue("RateLimiting:Enabled") ?? context.Configuration.GetValue("Security:RateLimiting:Enabled"); if (rateLimitEnabled == null) { return Task.FromResult(result .Info("Rate limiting configuration not found") .WithEvidence("Rate limiting", e => { e.Add("Configured", "false"); e.Add("Recommendation", "Consider enabling rate limiting for API protection"); }) .Build()); } if (rateLimitEnabled != true) { return Task.FromResult(result .Warn("Rate limiting is disabled") .WithEvidence("Rate limiting", e => { e.Add("Enabled", "false"); e.Add("Recommendation", "Enable rate limiting to prevent API abuse"); }) .WithCauses("Rate limiting explicitly disabled in configuration") .WithRemediation(r => r .AddManualStep(1, "Enable rate limiting", "Set RateLimiting:Enabled to true") .WithRunbookUrl("docs/doctor/articles/security/security-ratelimit.md")) .WithVerification("stella doctor --check check.security.ratelimit") .Build()); } var permitLimit = context.Configuration.GetValue("RateLimiting:PermitLimit") ?? context.Configuration.GetValue("Security:RateLimiting:PermitLimit") ?? 100; var windowSeconds = context.Configuration.GetValue("RateLimiting:WindowSeconds") ?? context.Configuration.GetValue("Security:RateLimiting:WindowSeconds") ?? 60; var queueLimit = context.Configuration.GetValue("RateLimiting:QueueLimit") ?? context.Configuration.GetValue("Security:RateLimiting:QueueLimit") ?? 0; var issues = new List(); if (permitLimit > 10000) { issues.Add($"Rate limit permit count ({permitLimit}) is very high"); } if (windowSeconds < 1) { issues.Add("Rate limit window is less than 1 second"); } else if (windowSeconds > 3600) { issues.Add($"Rate limit window ({windowSeconds}s) is very long - may not effectively prevent bursts"); } var requestsPerSecond = (double)permitLimit / windowSeconds; if (requestsPerSecond > 1000) { issues.Add($"Effective rate ({requestsPerSecond:F0} req/s) may be too permissive"); } if (issues.Count > 0) { return Task.FromResult(result .Warn($"{issues.Count} rate limiting configuration issue(s)") .WithEvidence("Rate limiting", e => { e.Add("Enabled", "true"); e.Add("PermitLimit", permitLimit.ToString(CultureInfo.InvariantCulture)); e.Add("WindowSeconds", windowSeconds.ToString(CultureInfo.InvariantCulture)); e.Add("QueueLimit", queueLimit.ToString(CultureInfo.InvariantCulture)); e.Add("EffectiveRatePerSecond", requestsPerSecond.ToString("F2", CultureInfo.InvariantCulture)); }) .WithCauses(issues.ToArray()) .Build()); } return Task.FromResult(result .Pass("Rate limiting is properly configured") .WithEvidence("Rate limiting", e => { e.Add("Enabled", "true"); e.Add("PermitLimit", permitLimit.ToString(CultureInfo.InvariantCulture)); e.Add("WindowSeconds", windowSeconds.ToString(CultureInfo.InvariantCulture)); e.Add("QueueLimit", queueLimit.ToString(CultureInfo.InvariantCulture)); e.Add("EffectiveRatePerSecond", requestsPerSecond.ToString("F2", CultureInfo.InvariantCulture)); }) .Build()); } }