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());
}
}