Implement remediation-aware health checks across all Doctor plugin modules (Agent, Attestor, Auth, BinaryAnalysis, Compliance, Crypto, Environment, EvidenceLocker, Notify, Observability, Operations, Policy, Postgres, Release, Scanner, Storage, Vex) and their backing library counterparts (AI, Attestation, Authority, Core, Cryptography, Database, Docker, Integration, Notify, Observability, Security, ServiceGraph, Sources, Verification). Each check now emits structured remediation metadata (severity, category, runbook links, and fix suggestions) consumed by the Doctor dashboard remediation panel. Also adds: - docs/doctor/articles/ knowledge base for check explanations - Advisory AI search seed and allowlist updates for doctor content - Sprint plan for doctor checks documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
135 lines
5.2 KiB
C#
135 lines
5.2 KiB
C#
|
|
using Microsoft.Extensions.Configuration;
|
|
using StellaOps.Doctor.Models;
|
|
using StellaOps.Doctor.Plugins;
|
|
using System.Globalization;
|
|
|
|
namespace StellaOps.Doctor.Plugins.Security.Checks;
|
|
|
|
/// <summary>
|
|
/// Validates rate limiting configuration.
|
|
/// </summary>
|
|
public sealed class RateLimitingCheck : IDoctorCheck
|
|
{
|
|
/// <inheritdoc />
|
|
public string CheckId => "check.security.ratelimit";
|
|
|
|
/// <inheritdoc />
|
|
public string Name => "Rate Limiting";
|
|
|
|
/// <inheritdoc />
|
|
public string Description => "Validates rate limiting is configured to prevent abuse";
|
|
|
|
/// <inheritdoc />
|
|
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<string> Tags => ["security", "ratelimit", "api"];
|
|
|
|
/// <inheritdoc />
|
|
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
|
|
|
|
/// <inheritdoc />
|
|
public bool CanRun(DoctorPluginContext context) => true;
|
|
|
|
/// <inheritdoc />
|
|
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
|
{
|
|
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
|
|
|
|
var rateLimitEnabled = context.Configuration.GetValue<bool?>("RateLimiting:Enabled")
|
|
?? context.Configuration.GetValue<bool?>("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<int?>("RateLimiting:PermitLimit")
|
|
?? context.Configuration.GetValue<int?>("Security:RateLimiting:PermitLimit")
|
|
?? 100;
|
|
|
|
var windowSeconds = context.Configuration.GetValue<int?>("RateLimiting:WindowSeconds")
|
|
?? context.Configuration.GetValue<int?>("Security:RateLimiting:WindowSeconds")
|
|
?? 60;
|
|
|
|
var queueLimit = context.Configuration.GetValue<int?>("RateLimiting:QueueLimit")
|
|
?? context.Configuration.GetValue<int?>("Security:RateLimiting:QueueLimit")
|
|
?? 0;
|
|
|
|
var issues = new List<string>();
|
|
|
|
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());
|
|
}
|
|
}
|