Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Security/Checks/TlsCertificateCheck.cs
master c58a236d70 Doctor plugin checks: implement health check classes and documentation
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>
2026-03-27 12:28:00 +02:00

171 lines
7.5 KiB
C#

using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
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")
.WithRunbookUrl("docs/doctor/articles/security/security-tls-certificate.md"))
.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")
.WithRunbookUrl("docs/doctor/articles/security/security-tls-certificate.md"))
.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")
.WithRunbookUrl("docs/doctor/articles/security/security-tls-certificate.md"))
.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());
}
}
}