Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Integration/Checks/LdapConnectivityCheck.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

167 lines
6.5 KiB
C#

using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using System.Globalization;
using System.Net.Sockets;
namespace StellaOps.Doctor.Plugins.Integration.Checks;
/// <summary>
/// Verifies connectivity to LDAP/Active Directory servers.
/// </summary>
public sealed class LdapConnectivityCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.integration.ldap";
/// <inheritdoc />
public string Name => "LDAP/AD Connectivity";
/// <inheritdoc />
public string Description => "Verifies connectivity to LDAP or Active Directory servers";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["connectivity", "ldap", "directory", "auth"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var host = context.Configuration.GetValue<string>("Ldap:Host")
?? context.Configuration.GetValue<string>("ActiveDirectory:Host")
?? context.Configuration.GetValue<string>("Authority:Ldap:Host");
return !string.IsNullOrWhiteSpace(host);
}
/// <inheritdoc />
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.integration", DoctorCategory.Integration.ToString());
var host = context.Configuration.GetValue<string>("Ldap:Host")
?? context.Configuration.GetValue<string>("ActiveDirectory:Host")
?? context.Configuration.GetValue<string>("Authority:Ldap:Host");
if (string.IsNullOrWhiteSpace(host))
{
return result
.Skip("LDAP not configured")
.WithEvidence("Configuration", e => e.Add("Ldap:Host", "(not set)"))
.Build();
}
var port = context.Configuration.GetValue<int?>("Ldap:Port")
?? context.Configuration.GetValue<int?>("ActiveDirectory:Port")
?? context.Configuration.GetValue<int?>("Authority:Ldap:Port")
?? 389;
var useSsl = context.Configuration.GetValue<bool?>("Ldap:UseSsl")
?? context.Configuration.GetValue<bool?>("ActiveDirectory:UseSsl")
?? false;
if (useSsl && port == 389)
{
port = 636;
}
try
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(host, port, ct);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), ct);
var completedTask = await Task.WhenAny(connectTask.AsTask(), timeoutTask);
if (completedTask == timeoutTask)
{
return result
.Fail($"Connection to LDAP server at {host}:{port} timed out")
.WithEvidence("LDAP connectivity", e =>
{
e.Add("Host", host);
e.Add("Port", port.ToString(CultureInfo.InvariantCulture));
e.Add("UseSsl", useSsl.ToString());
e.Add("Status", "timeout");
})
.WithCauses(
"LDAP server is not responding",
"Firewall blocking LDAP port",
"Network connectivity issues")
.WithRemediation(r => r
.AddManualStep(1, "Check LDAP server", "Verify LDAP server is running and accessible")
.AddManualStep(2, "Test connectivity", $"telnet {host} {port}")
.WithRunbookUrl("docs/doctor/articles/integration/integration-ldap.md"))
.WithVerification("stella doctor --check check.integration.ldap")
.Build();
}
await connectTask;
if (client.Connected)
{
return result
.Pass($"LDAP server reachable at {host}:{port}")
.WithEvidence("LDAP connectivity", e =>
{
e.Add("Host", host);
e.Add("Port", port.ToString(CultureInfo.InvariantCulture));
e.Add("UseSsl", useSsl.ToString());
e.Add("Status", "connected");
})
.Build();
}
return result
.Fail($"Failed to connect to LDAP server at {host}:{port}")
.WithEvidence("LDAP connectivity", e =>
{
e.Add("Host", host);
e.Add("Port", port.ToString(CultureInfo.InvariantCulture));
e.Add("Status", "connection_failed");
})
.Build();
}
catch (SocketException ex)
{
return result
.Fail($"Socket error connecting to LDAP: {ex.Message}")
.WithEvidence("LDAP connectivity", e =>
{
e.Add("Host", host);
e.Add("Port", port.ToString(CultureInfo.InvariantCulture));
e.Add("SocketErrorCode", ex.SocketErrorCode.ToString());
e.Add("Error", ex.Message);
})
.WithCauses(
"LDAP server is not running",
"DNS resolution failed",
"Network unreachable")
.WithRemediation(r => r
.AddManualStep(1, "Check LDAP configuration", "Verify Ldap:Host and Ldap:Port settings")
.AddManualStep(2, "Check DNS", $"nslookup {host}")
.WithRunbookUrl("docs/doctor/articles/integration/integration-ldap.md"))
.WithVerification("stella doctor --check check.integration.ldap")
.Build();
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return result
.Fail($"Error connecting to LDAP: {ex.Message}")
.WithEvidence("LDAP connectivity", e =>
{
e.Add("Host", host);
e.Add("Port", port.ToString(CultureInfo.InvariantCulture));
e.Add("ErrorType", ex.GetType().Name);
e.Add("Error", ex.Message);
})
.Build();
}
}
}