sln build fix (again), tests fixes, audit work and doctors work

This commit is contained in:
master
2026-01-12 22:15:51 +02:00
parent 9873f80830
commit 9330c64349
812 changed files with 48051 additions and 3891 deletions

View File

@@ -0,0 +1,147 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates API key security configuration.
/// </summary>
public sealed class ApiKeySecurityCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.apikey";
/// <inheritdoc />
public string Name => "API Key Security";
/// <inheritdoc />
public string Description => "Validates API key configuration and security practices";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "apikey", "authentication"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
return context.Configuration.GetSection("ApiKey").Exists()
|| context.Configuration.GetSection("Authentication:ApiKey").Exists()
|| context.Configuration.GetSection("Security:ApiKey").Exists();
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
var issues = new List<string>();
var apiKeyEnabled = context.Configuration.GetValue<bool?>("ApiKey:Enabled")
?? context.Configuration.GetValue<bool?>("Authentication:ApiKey:Enabled")
?? true;
var headerName = context.Configuration.GetValue<string>("ApiKey:HeaderName")
?? context.Configuration.GetValue<string>("Authentication:ApiKey:HeaderName")
?? "X-API-Key";
var minKeyLength = context.Configuration.GetValue<int?>("ApiKey:MinLength")
?? context.Configuration.GetValue<int?>("Authentication:ApiKey:MinLength")
?? 32;
var rateLimitPerKey = context.Configuration.GetValue<bool?>("ApiKey:RateLimitPerKey")
?? context.Configuration.GetValue<bool?>("Authentication:ApiKey:RateLimitPerKey")
?? false;
var keyRotationDays = context.Configuration.GetValue<int?>("ApiKey:RotationDays")
?? context.Configuration.GetValue<int?>("Authentication:ApiKey:RotationDays");
var allowInQueryString = context.Configuration.GetValue<bool?>("ApiKey:AllowInQueryString")
?? context.Configuration.GetValue<bool?>("Authentication:ApiKey:AllowQueryParam")
?? false;
if (apiKeyEnabled == false)
{
return Task.FromResult(result
.Info("API key authentication is disabled")
.WithEvidence("API key configuration", e =>
{
e.Add("Enabled", "false");
})
.Build());
}
if (minKeyLength < 32)
{
issues.Add($"Minimum API key length ({minKeyLength}) is too short - use at least 32 characters");
}
if (allowInQueryString == true)
{
issues.Add("API keys in query strings can be logged - use header-based authentication only");
}
if (headerName.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
{
issues.Add("Using 'Authorization' header for API keys may conflict with other auth schemes");
}
if (rateLimitPerKey != true)
{
issues.Add("Per-key rate limiting is not enabled - compromised keys could abuse the API");
}
if (keyRotationDays == null)
{
issues.Add("API key rotation policy is not configured");
}
else if (keyRotationDays > 365)
{
issues.Add($"API key rotation period ({keyRotationDays} days) is very long");
}
if (issues.Count > 0)
{
var hasCritical = issues.Any(i => i.Contains("too short") && minKeyLength < 16);
return Task.FromResult(result
.WithSeverity(hasCritical ? DoctorSeverity.Fail : DoctorSeverity.Warn,
$"{issues.Count} API key security issue(s)")
.WithEvidence("API key configuration", e =>
{
e.Add("Enabled", "true");
e.Add("HeaderName", headerName);
e.Add("MinKeyLength", minKeyLength.ToString(CultureInfo.InvariantCulture));
e.Add("AllowInQueryString", allowInQueryString.ToString()!);
e.Add("RateLimitPerKey", rateLimitPerKey.ToString()!);
e.Add("RotationDays", keyRotationDays?.ToString(CultureInfo.InvariantCulture) ?? "(not set)");
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Set minimum length", "Configure ApiKey:MinLength to at least 32")
.AddManualStep(2, "Disable query string", "Set ApiKey:AllowInQueryString to false")
.AddManualStep(3, "Enable rate limiting", "Set ApiKey:RateLimitPerKey to true"))
.WithVerification("stella doctor --check check.security.apikey")
.Build());
}
return Task.FromResult(result
.Pass("API key security is properly configured")
.WithEvidence("API key configuration", e =>
{
e.Add("Enabled", "true");
e.Add("HeaderName", headerName);
e.Add("MinKeyLength", minKeyLength.ToString(CultureInfo.InvariantCulture));
e.Add("AllowInQueryString", allowInQueryString.ToString()!);
e.Add("RateLimitPerKey", rateLimitPerKey.ToString()!);
e.Add("RotationDays", keyRotationDays?.ToString(CultureInfo.InvariantCulture) ?? "(not set)");
})
.Build());
}
}

View File

@@ -0,0 +1,128 @@
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates audit logging configuration.
/// </summary>
public sealed class AuditLoggingCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.audit.logging";
/// <inheritdoc />
public string Name => "Audit Logging";
/// <inheritdoc />
public string Description => "Validates audit logging is enabled for security events";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "audit", "logging"];
/// <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 issues = new List<string>();
var auditEnabled = context.Configuration.GetValue<bool?>("Audit:Enabled")
?? context.Configuration.GetValue<bool?>("Security:Audit:Enabled")
?? context.Configuration.GetValue<bool?>("Logging:Audit:Enabled");
var logAuthEvents = context.Configuration.GetValue<bool?>("Audit:LogAuthenticationEvents")
?? context.Configuration.GetValue<bool?>("Security:Audit:LogAuthEvents")
?? true;
var logAccessEvents = context.Configuration.GetValue<bool?>("Audit:LogAccessEvents")
?? context.Configuration.GetValue<bool?>("Security:Audit:LogDataAccess")
?? false;
var logAdminEvents = context.Configuration.GetValue<bool?>("Audit:LogAdministrativeEvents")
?? context.Configuration.GetValue<bool?>("Security:Audit:LogAdminActions")
?? true;
var auditDestination = context.Configuration.GetValue<string>("Audit:Destination")
?? context.Configuration.GetValue<string>("Security:Audit:Output");
if (auditEnabled == false)
{
return Task.FromResult(result
.Warn("Audit logging is explicitly disabled")
.WithEvidence("Audit configuration", e =>
{
e.Add("Enabled", "false");
e.Add("Recommendation", "Enable audit logging for security compliance");
})
.WithCauses("Audit logging disabled in configuration")
.WithRemediation(r => r
.AddManualStep(1, "Enable audit logging", "Set Audit:Enabled to true"))
.WithVerification("stella doctor --check check.security.audit.logging")
.Build());
}
if (auditEnabled == null)
{
issues.Add("Audit logging configuration not found - may not be enabled");
}
if (logAuthEvents != true)
{
issues.Add("Authentication events are not being logged");
}
if (logAdminEvents != true)
{
issues.Add("Administrative events are not being logged");
}
if (string.IsNullOrWhiteSpace(auditDestination))
{
issues.Add("Audit log destination is not configured");
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} audit logging issue(s)")
.WithEvidence("Audit configuration", e =>
{
e.Add("Enabled", auditEnabled?.ToString() ?? "(not set)");
e.Add("LogAuthenticationEvents", logAuthEvents.ToString()!);
e.Add("LogAccessEvents", logAccessEvents.ToString()!);
e.Add("LogAdministrativeEvents", logAdminEvents.ToString()!);
e.Add("Destination", auditDestination ?? "(not set)");
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Enable audit logging", "Set Audit:Enabled to true")
.AddManualStep(2, "Configure events", "Enable logging for auth, access, and admin events")
.AddManualStep(3, "Set destination", "Configure audit log destination"))
.WithVerification("stella doctor --check check.security.audit.logging")
.Build());
}
return Task.FromResult(result
.Pass("Audit logging is properly configured")
.WithEvidence("Audit configuration", e =>
{
e.Add("Enabled", "true");
e.Add("LogAuthenticationEvents", logAuthEvents.ToString()!);
e.Add("LogAccessEvents", logAccessEvents.ToString()!);
e.Add("LogAdministrativeEvents", logAdminEvents.ToString()!);
e.Add("Destination", auditDestination ?? "(console)");
})
.Build());
}
}

View File

@@ -0,0 +1,120 @@
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates CORS (Cross-Origin Resource Sharing) configuration.
/// </summary>
public sealed class CorsConfigurationCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.cors";
/// <inheritdoc />
public string Name => "CORS Configuration";
/// <inheritdoc />
public string Description => "Validates Cross-Origin Resource Sharing security settings";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "cors", "web"];
/// <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 issues = new List<string>();
var allowedOrigins = context.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? context.Configuration.GetSection("Security:Cors:AllowedOrigins").Get<string[]>()
?? [];
var allowCredentials = context.Configuration.GetValue<bool?>("Cors:AllowCredentials")
?? context.Configuration.GetValue<bool?>("Security:Cors:AllowCredentials")
?? false;
var allowAnyOrigin = context.Configuration.GetValue<bool?>("Cors:AllowAnyOrigin")
?? context.Configuration.GetValue<bool?>("Security:Cors:AllowAnyOrigin")
?? false;
var allowedMethods = context.Configuration.GetSection("Cors:AllowedMethods").Get<string[]>()
?? context.Configuration.GetSection("Security:Cors:AllowedMethods").Get<string[]>()
?? [];
if (allowAnyOrigin)
{
issues.Add("CORS allows any origin - this is insecure in production");
}
else if (allowedOrigins.Length == 0)
{
issues.Add("No CORS allowed origins configured");
}
else
{
foreach (var origin in allowedOrigins)
{
if (origin == "*")
{
issues.Add("CORS wildcard origin '*' is configured - this is insecure");
}
else if (origin.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !origin.Contains("localhost", StringComparison.OrdinalIgnoreCase)
&& !origin.Contains("127.0.0.1", StringComparison.OrdinalIgnoreCase))
{
issues.Add($"CORS allows non-HTTPS origin: {origin}");
}
}
}
if (allowAnyOrigin && allowCredentials)
{
issues.Add("CORS allows any origin with credentials - this is a critical security issue");
}
if (issues.Count > 0)
{
var hasCritical = issues.Any(i => i.Contains("critical") || (i.Contains("any origin") && !i.Contains("credentials")));
return Task.FromResult(result
.WithSeverity(hasCritical ? DoctorSeverity.Fail : DoctorSeverity.Warn,
$"{issues.Count} CORS configuration issue(s) found")
.WithEvidence("CORS configuration", e =>
{
e.Add("AllowAnyOrigin", allowAnyOrigin.ToString());
e.Add("AllowedOriginsCount", allowedOrigins.Length.ToString());
e.Add("AllowedOrigins", allowedOrigins.Length > 0 ? string.Join(", ", allowedOrigins.Take(5)) : "(none)");
e.Add("AllowCredentials", allowCredentials.ToString());
e.Add("AllowedMethods", allowedMethods.Length > 0 ? string.Join(", ", allowedMethods) : "(default)");
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Specify origins", "Configure explicit allowed origins in Cors:AllowedOrigins")
.AddManualStep(2, "Use HTTPS", "Ensure all allowed origins use HTTPS"))
.WithVerification("stella doctor --check check.security.cors")
.Build());
}
return Task.FromResult(result
.Pass("CORS configuration is secure")
.WithEvidence("CORS configuration", e =>
{
e.Add("AllowAnyOrigin", allowAnyOrigin.ToString());
e.Add("AllowedOriginsCount", allowedOrigins.Length.ToString());
e.Add("AllowedOrigins", allowedOrigins.Length > 0 ? string.Join(", ", allowedOrigins.Take(5)) : "(none)");
e.Add("AllowCredentials", allowCredentials.ToString());
})
.Build());
}
}

View File

@@ -0,0 +1,114 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates encryption key configuration and rotation.
/// </summary>
public sealed class EncryptionKeyCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.encryption";
/// <inheritdoc />
public string Name => "Encryption Keys";
/// <inheritdoc />
public string Description => "Validates encryption key configuration and algorithms";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "encryption", "cryptography"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
return context.Configuration.GetSection("Encryption").Exists()
|| context.Configuration.GetSection("DataProtection").Exists()
|| context.Configuration.GetSection("Cryptography").Exists();
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
var issues = new List<string>();
var algorithm = context.Configuration.GetValue<string>("Encryption:Algorithm")
?? context.Configuration.GetValue<string>("Cryptography:SymmetricAlgorithm")
?? "AES-256";
var keySize = context.Configuration.GetValue<int?>("Encryption:KeySize")
?? context.Configuration.GetValue<int?>("Cryptography:KeySize");
var rotationDays = context.Configuration.GetValue<int?>("Encryption:KeyRotationDays")
?? context.Configuration.GetValue<int?>("Cryptography:KeyRotationDays");
var dataProtectionPath = context.Configuration.GetValue<string>("DataProtection:KeysPath");
var weakAlgorithms = new[] { "DES", "3DES", "RC4", "MD5", "SHA1" };
if (weakAlgorithms.Any(wa => algorithm.Contains(wa, StringComparison.OrdinalIgnoreCase)))
{
issues.Add($"Weak encryption algorithm configured: {algorithm}");
}
if (keySize.HasValue && keySize < 128)
{
issues.Add($"Encryption key size ({keySize} bits) is too small - use at least 128 bits");
}
if (rotationDays.HasValue && rotationDays > 365)
{
issues.Add($"Key rotation period ({rotationDays} days) is very long - consider more frequent rotation");
}
if (!string.IsNullOrWhiteSpace(dataProtectionPath))
{
if (!Directory.Exists(dataProtectionPath))
{
issues.Add($"Data protection keys path does not exist: {dataProtectionPath}");
}
}
if (issues.Count > 0)
{
var hasCritical = issues.Any(i => i.Contains("Weak encryption") || i.Contains("too small"));
return Task.FromResult(result
.WithSeverity(hasCritical ? DoctorSeverity.Fail : DoctorSeverity.Warn,
$"{issues.Count} encryption configuration issue(s)")
.WithEvidence("Encryption configuration", e =>
{
e.Add("Algorithm", algorithm);
e.Add("KeySize", keySize?.ToString(CultureInfo.InvariantCulture) ?? "(default)");
e.Add("KeyRotationDays", rotationDays?.ToString(CultureInfo.InvariantCulture) ?? "(not set)");
e.Add("DataProtectionPath", dataProtectionPath ?? "(not set)");
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Use strong algorithm", "Configure AES-256 or stronger")
.AddManualStep(2, "Set key rotation", "Configure Encryption:KeyRotationDays"))
.WithVerification("stella doctor --check check.security.encryption")
.Build());
}
return Task.FromResult(result
.Pass("Encryption configuration is secure")
.WithEvidence("Encryption configuration", e =>
{
e.Add("Algorithm", algorithm);
e.Add("KeySize", keySize?.ToString(CultureInfo.InvariantCulture) ?? "(default)");
e.Add("KeyRotationDays", rotationDays?.ToString(CultureInfo.InvariantCulture) ?? "(not set)");
})
.Build());
}
}

View File

@@ -0,0 +1,135 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates JWT token configuration and security settings.
/// </summary>
public sealed class JwtConfigurationCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.jwt.config";
/// <inheritdoc />
public string Name => "JWT Configuration";
/// <inheritdoc />
public string Description => "Validates JWT token signing and validation configuration";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "jwt", "authentication"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var jwtEnabled = context.Configuration.GetSection("Jwt").Exists()
|| context.Configuration.GetSection("Authentication:Jwt").Exists();
return jwtEnabled;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
var issues = new List<string>();
var signingKey = context.Configuration.GetValue<string>("Jwt:SigningKey")
?? context.Configuration.GetValue<string>("Authentication:Jwt:SigningKey");
var issuer = context.Configuration.GetValue<string>("Jwt:Issuer")
?? context.Configuration.GetValue<string>("Authentication:Jwt:Issuer");
var audience = context.Configuration.GetValue<string>("Jwt:Audience")
?? context.Configuration.GetValue<string>("Authentication:Jwt:Audience");
var expirationMinutes = context.Configuration.GetValue<int?>("Jwt:ExpirationMinutes")
?? context.Configuration.GetValue<int?>("Authentication:Jwt:ExpirationMinutes")
?? 60;
var algorithm = context.Configuration.GetValue<string>("Jwt:Algorithm")
?? context.Configuration.GetValue<string>("Authentication:Jwt:Algorithm")
?? "HS256";
if (string.IsNullOrWhiteSpace(signingKey))
{
issues.Add("JWT signing key is not configured");
}
else if (signingKey.Length < 32)
{
issues.Add($"JWT signing key is too short ({signingKey.Length} chars) - minimum 32 characters recommended");
}
if (string.IsNullOrWhiteSpace(issuer))
{
issues.Add("JWT issuer is not configured");
}
if (string.IsNullOrWhiteSpace(audience))
{
issues.Add("JWT audience is not configured");
}
if (expirationMinutes > 1440)
{
issues.Add($"JWT expiration ({expirationMinutes} minutes) is very long - consider shorter token lifetime");
}
var weakAlgorithms = new[] { "none", "HS256" };
if (weakAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase))
{
if (algorithm.Equals("none", StringComparison.OrdinalIgnoreCase))
{
issues.Add("JWT algorithm 'none' is insecure - use RS256 or ES256");
}
else
{
issues.Add($"JWT algorithm '{algorithm}' is acceptable but RS256/ES256 recommended for production");
}
}
if (issues.Count > 0)
{
var severity = issues.Any(i => i.Contains("not configured") || i.Contains("'none'"))
? DoctorSeverity.Fail
: DoctorSeverity.Warn;
return Task.FromResult(result
.WithSeverity(severity, $"{issues.Count} JWT configuration issue(s) found")
.WithEvidence("JWT configuration", e =>
{
e.Add("SigningKeyConfigured", (!string.IsNullOrWhiteSpace(signingKey)).ToString());
e.Add("SigningKeyLength", signingKey?.Length.ToString(CultureInfo.InvariantCulture) ?? "0");
e.Add("Issuer", issuer ?? "(not set)");
e.Add("Audience", audience ?? "(not set)");
e.Add("ExpirationMinutes", expirationMinutes.ToString(CultureInfo.InvariantCulture));
e.Add("Algorithm", algorithm);
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Configure JWT settings", "Set Jwt:SigningKey, Jwt:Issuer, and Jwt:Audience")
.AddManualStep(2, "Use strong key", "Ensure signing key is at least 32 characters")
.AddManualStep(3, "Consider RS256", "Use asymmetric algorithms for production"))
.WithVerification("stella doctor --check check.security.jwt.config")
.Build());
}
return Task.FromResult(result
.Pass("JWT configuration is secure")
.WithEvidence("JWT configuration", e =>
{
e.Add("SigningKeyConfigured", "true");
e.Add("SigningKeyLength", signingKey?.Length.ToString(CultureInfo.InvariantCulture) ?? "0");
e.Add("Issuer", issuer ?? "(not set)");
e.Add("Audience", audience ?? "(not set)");
e.Add("ExpirationMinutes", expirationMinutes.ToString(CultureInfo.InvariantCulture));
e.Add("Algorithm", algorithm);
})
.Build());
}
}

View File

@@ -0,0 +1,153 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates password policy configuration.
/// </summary>
public sealed class PasswordPolicyCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.password.policy";
/// <inheritdoc />
public string Name => "Password Policy";
/// <inheritdoc />
public string Description => "Validates password requirements meet security standards";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "password", "authentication"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
return context.Configuration.GetSection("Identity:Password").Exists()
|| context.Configuration.GetSection("Password").Exists()
|| context.Configuration.GetSection("Security:Password").Exists();
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
var issues = new List<string>();
var minLength = context.Configuration.GetValue<int?>("Identity:Password:RequiredLength")
?? context.Configuration.GetValue<int?>("Password:MinLength")
?? context.Configuration.GetValue<int?>("Security:Password:MinLength")
?? 8;
var requireDigit = context.Configuration.GetValue<bool?>("Identity:Password:RequireDigit")
?? context.Configuration.GetValue<bool?>("Password:RequireDigit")
?? true;
var requireLowercase = context.Configuration.GetValue<bool?>("Identity:Password:RequireLowercase")
?? context.Configuration.GetValue<bool?>("Password:RequireLowercase")
?? true;
var requireUppercase = context.Configuration.GetValue<bool?>("Identity:Password:RequireUppercase")
?? context.Configuration.GetValue<bool?>("Password:RequireUppercase")
?? true;
var requireNonAlphanumeric = context.Configuration.GetValue<bool?>("Identity:Password:RequireNonAlphanumeric")
?? context.Configuration.GetValue<bool?>("Password:RequireSpecialChar")
?? true;
var maxFailedAttempts = context.Configuration.GetValue<int?>("Identity:Lockout:MaxFailedAccessAttempts")
?? context.Configuration.GetValue<int?>("Security:Lockout:MaxAttempts")
?? 5;
var lockoutDurationMinutes = context.Configuration.GetValue<int?>("Identity:Lockout:DefaultLockoutTimeSpan")
?? context.Configuration.GetValue<int?>("Security:Lockout:DurationMinutes")
?? 5;
if (minLength < 8)
{
issues.Add($"Minimum password length ({minLength}) is too short - use at least 8 characters");
}
else if (minLength < 12)
{
issues.Add($"Password length ({minLength}) is acceptable but 12+ is recommended");
}
if (requireDigit != true)
{
issues.Add("Password policy does not require digits");
}
if (requireLowercase != true)
{
issues.Add("Password policy does not require lowercase letters");
}
if (requireUppercase != true)
{
issues.Add("Password policy does not require uppercase letters");
}
if (requireNonAlphanumeric != true)
{
issues.Add("Password policy does not require special characters");
}
if (maxFailedAttempts > 10)
{
issues.Add($"Max failed attempts ({maxFailedAttempts}) before lockout is too high");
}
if (lockoutDurationMinutes < 1)
{
issues.Add("Account lockout duration is less than 1 minute");
}
if (issues.Count > 0)
{
var hasCritical = issues.Any(i => i.Contains("too short") && minLength < 6);
return Task.FromResult(result
.WithSeverity(hasCritical ? DoctorSeverity.Fail : DoctorSeverity.Warn,
$"{issues.Count} password policy issue(s)")
.WithEvidence("Password policy", e =>
{
e.Add("MinLength", minLength.ToString(CultureInfo.InvariantCulture));
e.Add("RequireDigit", requireDigit.ToString()!);
e.Add("RequireLowercase", requireLowercase.ToString()!);
e.Add("RequireUppercase", requireUppercase.ToString()!);
e.Add("RequireSpecialChar", requireNonAlphanumeric.ToString()!);
e.Add("MaxFailedAttempts", maxFailedAttempts.ToString(CultureInfo.InvariantCulture));
e.Add("LockoutDurationMinutes", lockoutDurationMinutes.ToString(CultureInfo.InvariantCulture));
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Increase minimum length", "Set Identity:Password:RequiredLength to at least 12")
.AddManualStep(2, "Enable complexity", "Require digits, uppercase, lowercase, and special characters"))
.WithVerification("stella doctor --check check.security.password.policy")
.Build());
}
return Task.FromResult(result
.Pass("Password policy meets security standards")
.WithEvidence("Password policy", e =>
{
e.Add("MinLength", minLength.ToString(CultureInfo.InvariantCulture));
e.Add("RequireDigit", requireDigit.ToString()!);
e.Add("RequireLowercase", requireLowercase.ToString()!);
e.Add("RequireUppercase", requireUppercase.ToString()!);
e.Add("RequireSpecialChar", requireNonAlphanumeric.ToString()!);
e.Add("MaxFailedAttempts", maxFailedAttempts.ToString(CultureInfo.InvariantCulture));
e.Add("LockoutDurationMinutes", lockoutDurationMinutes.ToString(CultureInfo.InvariantCulture));
})
.Build());
}
}

View File

@@ -0,0 +1,132 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
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"))
.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());
}
}

View File

@@ -0,0 +1,129 @@
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates secrets management configuration.
/// </summary>
public sealed class SecretsConfigurationCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.secrets";
/// <inheritdoc />
public string Name => "Secrets Configuration";
/// <inheritdoc />
public string Description => "Validates secrets are properly managed and not exposed in configuration";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "secrets", "configuration"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <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 issues = new List<string>();
var sensitiveKeys = new[]
{
"ConnectionStrings:Default",
"Database:ConnectionString",
"Jwt:SigningKey",
"Jwt:Secret",
"ApiKey",
"ApiSecret",
"S3:SecretKey",
"Smtp:Password",
"Ldap:Password",
"Redis:Password",
"Valkey:Password"
};
foreach (var key in sensitiveKeys)
{
var value = context.Configuration.GetValue<string>(key);
if (!string.IsNullOrWhiteSpace(value))
{
if (IsPlainTextSecret(value))
{
issues.Add($"Potential plain text secret in configuration: {key}");
}
}
}
var secretsProvider = context.Configuration.GetValue<string>("Secrets:Provider")
?? context.Configuration.GetValue<string>("KeyVault:Provider");
var vaultUrl = context.Configuration.GetValue<string>("Secrets:VaultUrl")
?? context.Configuration.GetValue<string>("KeyVault:Url")
?? context.Configuration.GetValue<string>("Vault:Address");
var useSecretManager = context.Configuration.GetValue<bool?>("Secrets:UseSecretManager")
?? vaultUrl != null
|| secretsProvider != null;
if (!useSecretManager && issues.Count > 0)
{
issues.Add("No secrets management provider configured");
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Fail($"{issues.Count} secrets management issue(s) found")
.WithEvidence("Secrets configuration", e =>
{
e.Add("SecretsProvider", secretsProvider ?? "(not set)");
e.Add("VaultConfigured", (vaultUrl != null).ToString());
e.Add("PotentialIssuesFound", issues.Count.ToString());
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Use secrets manager", "Configure a secrets provider like HashiCorp Vault or Azure Key Vault")
.AddManualStep(2, "Use environment variables", "Move secrets to environment variables")
.AddManualStep(3, "Use user secrets", "Use dotnet user-secrets for development"))
.WithVerification("stella doctor --check check.security.secrets")
.Build());
}
return Task.FromResult(result
.Pass("Secrets configuration appears secure")
.WithEvidence("Secrets configuration", e =>
{
e.Add("SecretsProvider", secretsProvider ?? "environment/user-secrets");
e.Add("VaultConfigured", (vaultUrl != null).ToString());
e.Add("PlainTextSecretsFound", "0");
})
.Build());
}
private static bool IsPlainTextSecret(string value)
{
if (value.StartsWith("vault:", StringComparison.OrdinalIgnoreCase)) return false;
if (value.StartsWith("azurekv:", StringComparison.OrdinalIgnoreCase)) return false;
if (value.StartsWith("aws:", StringComparison.OrdinalIgnoreCase)) return false;
if (value.StartsWith("gcp:", StringComparison.OrdinalIgnoreCase)) return false;
if (value.StartsWith("${", StringComparison.Ordinal)) return false;
if (value.StartsWith("@Microsoft.KeyVault", StringComparison.OrdinalIgnoreCase)) return false;
if (value.Length < 8) return false;
var hasUpperAndLower = value.Any(char.IsUpper) && value.Any(char.IsLower);
var hasSpecialOrDigit = value.Any(char.IsDigit) || value.Any(c => !char.IsLetterOrDigit(c));
return hasUpperAndLower && hasSpecialOrDigit;
}
}

View File

@@ -0,0 +1,117 @@
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates HTTP security headers configuration.
/// </summary>
public sealed class SecurityHeadersCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.headers";
/// <inheritdoc />
public string Name => "Security Headers";
/// <inheritdoc />
public string Description => "Validates HTTP security headers are properly configured";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "headers", "web"];
/// <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 issues = new List<string>();
var hsts = context.Configuration.GetValue<bool?>("Security:Headers:Hsts:Enabled")
?? context.Configuration.GetValue<bool?>("Hsts:Enabled");
var xFrameOptions = context.Configuration.GetValue<string>("Security:Headers:XFrameOptions")
?? context.Configuration.GetValue<string>("Headers:XFrameOptions");
var contentSecurityPolicy = context.Configuration.GetValue<string>("Security:Headers:ContentSecurityPolicy")
?? context.Configuration.GetValue<string>("Headers:Csp");
var xContentTypeOptions = context.Configuration.GetValue<bool?>("Security:Headers:XContentTypeOptions:Enabled")
?? context.Configuration.GetValue<bool?>("Headers:XContentTypeOptions");
var referrerPolicy = context.Configuration.GetValue<string>("Security:Headers:ReferrerPolicy")
?? context.Configuration.GetValue<string>("Headers:ReferrerPolicy");
if (hsts != true)
{
issues.Add("HSTS (HTTP Strict Transport Security) is not enabled");
}
if (string.IsNullOrWhiteSpace(xFrameOptions))
{
issues.Add("X-Frame-Options header is not configured (clickjacking protection)");
}
else if (xFrameOptions.Equals("ALLOWALL", StringComparison.OrdinalIgnoreCase))
{
issues.Add("X-Frame-Options is set to ALLOWALL - this provides no protection");
}
if (string.IsNullOrWhiteSpace(contentSecurityPolicy))
{
issues.Add("Content-Security-Policy header is not configured");
}
if (xContentTypeOptions != true)
{
issues.Add("X-Content-Type-Options: nosniff is not enabled (MIME type sniffing protection)");
}
if (string.IsNullOrWhiteSpace(referrerPolicy))
{
issues.Add("Referrer-Policy header is not configured");
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} security header(s) not configured")
.WithEvidence("Security headers", e =>
{
e.Add("HSTS", hsts == true ? "enabled" : "not enabled");
e.Add("X-Frame-Options", xFrameOptions ?? "(not set)");
e.Add("Content-Security-Policy", string.IsNullOrWhiteSpace(contentSecurityPolicy) ? "(not set)" : "configured");
e.Add("X-Content-Type-Options", xContentTypeOptions == true ? "nosniff" : "(not set)");
e.Add("Referrer-Policy", referrerPolicy ?? "(not set)");
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Enable HSTS", "Set Security:Headers:Hsts:Enabled to true")
.AddManualStep(2, "Set X-Frame-Options", "Configure as DENY or SAMEORIGIN")
.AddManualStep(3, "Configure CSP", "Set a Content-Security-Policy appropriate for your app"))
.WithVerification("stella doctor --check check.security.headers")
.Build());
}
return Task.FromResult(result
.Pass("Security headers are properly configured")
.WithEvidence("Security headers", e =>
{
e.Add("HSTS", "enabled");
e.Add("X-Frame-Options", xFrameOptions ?? "(not set)");
e.Add("Content-Security-Policy", "configured");
e.Add("X-Content-Type-Options", "nosniff");
e.Add("Referrer-Policy", referrerPolicy ?? "(not set)");
})
.Build());
}
}

View File

@@ -0,0 +1,166 @@
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.Checks;
/// <summary>
/// Validates TLS certificate configuration and expiration.
/// </summary>
public sealed class TlsCertificateCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.security.tls.certificate";
/// <inheritdoc />
public string Name => "TLS Certificate";
/// <inheritdoc />
public string Description => "Validates TLS certificate validity and expiration";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "tls", "certificate"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var certPath = context.Configuration.GetValue<string>("Tls:CertificatePath")
?? context.Configuration.GetValue<string>("Kestrel:Certificates:Default:Path");
return !string.IsNullOrWhiteSpace(certPath);
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.security", DoctorCategory.Security.ToString());
var certPath = context.Configuration.GetValue<string>("Tls:CertificatePath")
?? context.Configuration.GetValue<string>("Kestrel:Certificates:Default:Path");
if (string.IsNullOrWhiteSpace(certPath))
{
return Task.FromResult(result
.Skip("TLS certificate path not configured")
.WithEvidence("Configuration", e => e.Add("CertificatePath", "(not set)"))
.Build());
}
if (!File.Exists(certPath))
{
return Task.FromResult(result
.Fail($"TLS certificate file not found: {certPath}")
.WithEvidence("TLS configuration", e =>
{
e.Add("CertificatePath", certPath);
e.Add("FileExists", "false");
})
.WithCauses("Certificate file path is incorrect", "Certificate file was deleted")
.WithRemediation(r => r
.AddManualStep(1, "Verify path", "Check Tls:CertificatePath configuration")
.AddManualStep(2, "Generate certificate", "Generate or obtain a valid TLS certificate"))
.WithVerification("stella doctor --check check.security.tls.certificate")
.Build());
}
try
{
var certPassword = context.Configuration.GetValue<string>("Tls:CertificatePassword")
?? context.Configuration.GetValue<string>("Kestrel:Certificates:Default:Password");
using var cert = string.IsNullOrEmpty(certPassword)
? X509CertificateLoader.LoadCertificateFromFile(certPath)
: X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword);
var now = context.TimeProvider.GetUtcNow();
var daysUntilExpiry = (cert.NotAfter - now.DateTime).TotalDays;
if (now.DateTime < cert.NotBefore)
{
return Task.FromResult(result
.Fail("TLS certificate is not yet valid")
.WithEvidence("TLS certificate", e =>
{
e.Add("Subject", cert.Subject);
e.Add("Issuer", cert.Issuer);
e.Add("NotBefore", cert.NotBefore.ToString("O", CultureInfo.InvariantCulture));
e.Add("NotAfter", cert.NotAfter.ToString("O", CultureInfo.InvariantCulture));
})
.WithCauses("Certificate validity period has not started")
.Build());
}
if (now.DateTime > cert.NotAfter)
{
return Task.FromResult(result
.Fail("TLS certificate has expired")
.WithEvidence("TLS certificate", e =>
{
e.Add("Subject", cert.Subject);
e.Add("Issuer", cert.Issuer);
e.Add("ExpiredOn", cert.NotAfter.ToString("O", CultureInfo.InvariantCulture));
e.Add("DaysExpired", Math.Abs(daysUntilExpiry).ToString("F0", CultureInfo.InvariantCulture));
})
.WithCauses("Certificate has exceeded its validity period")
.WithRemediation(r => r
.AddManualStep(1, "Renew certificate", "Obtain a new TLS certificate")
.AddManualStep(2, "Update configuration", "Update Tls:CertificatePath with new certificate"))
.WithVerification("stella doctor --check check.security.tls.certificate")
.Build());
}
if (daysUntilExpiry < 30)
{
return Task.FromResult(result
.Warn($"TLS certificate expires in {daysUntilExpiry:F0} days")
.WithEvidence("TLS certificate", e =>
{
e.Add("Subject", cert.Subject);
e.Add("Issuer", cert.Issuer);
e.Add("NotAfter", cert.NotAfter.ToString("O", CultureInfo.InvariantCulture));
e.Add("DaysUntilExpiry", daysUntilExpiry.ToString("F0", CultureInfo.InvariantCulture));
})
.WithCauses("Certificate is approaching expiration")
.WithRemediation(r => r
.AddManualStep(1, "Plan renewal", "Schedule certificate renewal before expiration"))
.Build());
}
return Task.FromResult(result
.Pass($"TLS certificate valid for {daysUntilExpiry:F0} days")
.WithEvidence("TLS certificate", e =>
{
e.Add("Subject", cert.Subject);
e.Add("Issuer", cert.Issuer);
e.Add("NotBefore", cert.NotBefore.ToString("O", CultureInfo.InvariantCulture));
e.Add("NotAfter", cert.NotAfter.ToString("O", CultureInfo.InvariantCulture));
e.Add("DaysUntilExpiry", daysUntilExpiry.ToString("F0", CultureInfo.InvariantCulture));
e.Add("Thumbprint", cert.Thumbprint);
})
.Build());
}
catch (Exception ex)
{
return Task.FromResult(result
.Fail($"Failed to load TLS certificate: {ex.Message}")
.WithEvidence("TLS configuration", e =>
{
e.Add("CertificatePath", certPath);
e.Add("ErrorType", ex.GetType().Name);
e.Add("Error", ex.Message);
})
.WithCauses(
"Certificate file is corrupted",
"Certificate password is incorrect",
"Certificate format not supported")
.Build());
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Security.DependencyInjection;
/// <summary>
/// Extension methods for registering the Security plugin.
/// </summary>
public static class SecurityPluginExtensions
{
/// <summary>
/// Adds the Doctor Security plugin to the service collection.
/// </summary>
public static IServiceCollection AddDoctorSecurityPlugin(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDoctorPlugin, SecurityPlugin>());
return services;
}
}

View File

@@ -0,0 +1,47 @@
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Security.Checks;
namespace StellaOps.Doctor.Plugins.Security;
/// <summary>
/// Plugin for security configuration diagnostics.
/// </summary>
public sealed class SecurityPlugin : IDoctorPlugin
{
/// <inheritdoc />
public string PluginId => "stellaops.doctor.security";
/// <inheritdoc />
public string DisplayName => "Security Configuration";
/// <inheritdoc />
public DoctorCategory Category => DoctorCategory.Security;
/// <inheritdoc />
public Version Version => new(1, 0, 0);
/// <inheritdoc />
public Version MinEngineVersion => new(1, 0, 0);
/// <inheritdoc />
public bool IsAvailable(IServiceProvider services) => true;
/// <inheritdoc />
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) =>
[
new TlsCertificateCheck(),
new JwtConfigurationCheck(),
new CorsConfigurationCheck(),
new RateLimitingCheck(),
new SecurityHeadersCheck(),
new SecretsConfigurationCheck(),
new EncryptionKeyCheck(),
new PasswordPolicyCheck(),
new AuditLoggingCheck(),
new ApiKeySecurityCheck()
];
/// <inheritdoc />
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>