audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -0,0 +1,48 @@
namespace StellaOps.Doctor.Detection;
/// <summary>
/// Detects the runtime environment where Stella Ops is deployed.
/// Used to generate runtime-specific remediation commands.
/// </summary>
public interface IRuntimeDetector
{
/// <summary>
/// Detects the current runtime environment.
/// </summary>
/// <returns>The detected runtime environment.</returns>
RuntimeEnvironment Detect();
/// <summary>
/// Checks if Docker is available on the system.
/// </summary>
bool IsDockerAvailable();
/// <summary>
/// Checks if running within a Kubernetes cluster.
/// </summary>
bool IsKubernetesContext();
/// <summary>
/// Checks if a specific service is managed by systemd.
/// </summary>
/// <param name="serviceName">The name of the service to check.</param>
bool IsSystemdManaged(string serviceName);
/// <summary>
/// Gets the Docker Compose project path if available.
/// </summary>
/// <returns>The compose file path, or null if not found.</returns>
string? GetComposeProjectPath();
/// <summary>
/// Gets the current Kubernetes namespace.
/// </summary>
/// <returns>The namespace, or null if not in Kubernetes.</returns>
string? GetKubernetesNamespace();
/// <summary>
/// Gets environment-specific context values.
/// </summary>
/// <returns>Dictionary of context key-value pairs.</returns>
IReadOnlyDictionary<string, string> GetContextValues();
}

View File

@@ -0,0 +1,339 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace StellaOps.Doctor.Detection;
/// <summary>
/// Default implementation of <see cref="IRuntimeDetector"/>.
/// Detects Docker Compose, Kubernetes, systemd, and Windows Service environments.
/// </summary>
public sealed class RuntimeDetector : IRuntimeDetector
{
private readonly ILogger<RuntimeDetector> _logger;
private readonly Lazy<RuntimeEnvironment> _detectedRuntime;
private readonly Lazy<IReadOnlyDictionary<string, string>> _contextValues;
private static readonly string[] ComposeFileNames =
[
"docker-compose.yml",
"docker-compose.yaml",
"compose.yml",
"compose.yaml"
];
private static readonly string[] ComposeSearchPaths =
[
".",
"..",
"devops/compose",
"../devops/compose"
];
public RuntimeDetector(ILogger<RuntimeDetector> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_detectedRuntime = new Lazy<RuntimeEnvironment>(DetectInternal);
_contextValues = new Lazy<IReadOnlyDictionary<string, string>>(BuildContextValues);
}
/// <inheritdoc />
public RuntimeEnvironment Detect() => _detectedRuntime.Value;
/// <inheritdoc />
public bool IsDockerAvailable()
{
try
{
// Check for docker command
var startInfo = new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "docker.exe" : "docker",
Arguments = "--version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.WaitForExit(5000);
return process.ExitCode == 0;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Docker availability check failed");
return false;
}
}
/// <inheritdoc />
public bool IsKubernetesContext()
{
// Check for KUBERNETES_SERVICE_HOST environment variable
var kubeHost = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
if (!string.IsNullOrEmpty(kubeHost))
{
_logger.LogDebug("Detected Kubernetes via KUBERNETES_SERVICE_HOST: {Host}", kubeHost);
return true;
}
// Check for kubeconfig file
var kubeConfig = Environment.GetEnvironmentVariable("KUBECONFIG");
if (!string.IsNullOrEmpty(kubeConfig) && File.Exists(kubeConfig))
{
_logger.LogDebug("Detected Kubernetes via KUBECONFIG: {Path}", kubeConfig);
return true;
}
// Check default kubeconfig location
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var defaultKubeConfig = Path.Combine(homeDir, ".kube", "config");
if (File.Exists(defaultKubeConfig))
{
_logger.LogDebug("Detected Kubernetes via default kubeconfig: {Path}", defaultKubeConfig);
return true;
}
return false;
}
/// <inheritdoc />
public bool IsSystemdManaged(string serviceName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
try
{
var startInfo = new ProcessStartInfo
{
FileName = "systemctl",
Arguments = $"is-enabled {serviceName}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.WaitForExit(5000);
return process.ExitCode == 0;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "systemd check failed for service {ServiceName}", serviceName);
return false;
}
}
/// <inheritdoc />
public string? GetComposeProjectPath()
{
// Check COMPOSE_FILE environment variable
var composeFile = Environment.GetEnvironmentVariable("COMPOSE_FILE");
if (!string.IsNullOrEmpty(composeFile) && File.Exists(composeFile))
{
return composeFile;
}
// Search common locations
var currentDir = Directory.GetCurrentDirectory();
foreach (var searchPath in ComposeSearchPaths)
{
var searchDir = Path.GetFullPath(Path.Combine(currentDir, searchPath));
if (!Directory.Exists(searchDir)) continue;
foreach (var fileName in ComposeFileNames)
{
var fullPath = Path.Combine(searchDir, fileName);
if (File.Exists(fullPath))
{
_logger.LogDebug("Found Docker Compose file at: {Path}", fullPath);
return fullPath;
}
}
}
return null;
}
/// <inheritdoc />
public string? GetKubernetesNamespace()
{
// Check environment variable
var ns = Environment.GetEnvironmentVariable("KUBERNETES_NAMESPACE");
if (!string.IsNullOrEmpty(ns))
{
return ns;
}
// Check namespace file (mounted in pods)
const string namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
if (File.Exists(namespaceFile))
{
try
{
return File.ReadAllText(namespaceFile).Trim();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read Kubernetes namespace file");
}
}
// Default namespace
return "stellaops";
}
/// <inheritdoc />
public IReadOnlyDictionary<string, string> GetContextValues() => _contextValues.Value;
private RuntimeEnvironment DetectInternal()
{
_logger.LogDebug("Detecting runtime environment...");
// Check if running in Docker container
if (File.Exists("/.dockerenv"))
{
_logger.LogInformation("Detected Docker container environment");
return RuntimeEnvironment.DockerCompose;
}
// Check for Kubernetes
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST")))
{
_logger.LogInformation("Detected Kubernetes environment");
return RuntimeEnvironment.Kubernetes;
}
// Check for Docker Compose
if (IsDockerAvailable() && GetComposeProjectPath() != null)
{
_logger.LogInformation("Detected Docker Compose environment");
return RuntimeEnvironment.DockerCompose;
}
// Check for systemd (Linux)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "systemctl",
Arguments = "--version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
process.WaitForExit(5000);
if (process.ExitCode == 0)
{
_logger.LogInformation("Detected systemd environment");
return RuntimeEnvironment.Systemd;
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "systemd detection failed");
}
}
// Check for Windows Service
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// If running as a service, parent process is services.exe
try
{
using var current = Process.GetCurrentProcess();
var parentId = GetParentProcessId(current.Id);
if (parentId > 0)
{
using var parent = Process.GetProcessById(parentId);
if (parent.ProcessName.Equals("services", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Detected Windows Service environment");
return RuntimeEnvironment.WindowsService;
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Windows Service detection failed");
}
}
_logger.LogInformation("Detected bare/manual environment");
return RuntimeEnvironment.Bare;
}
private IReadOnlyDictionary<string, string> BuildContextValues()
{
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var runtime = Detect();
// Common values
values["RUNTIME"] = runtime.ToString();
switch (runtime)
{
case RuntimeEnvironment.DockerCompose:
var composePath = GetComposeProjectPath();
if (composePath != null)
{
values["COMPOSE_FILE"] = composePath;
}
var projectName = Environment.GetEnvironmentVariable("COMPOSE_PROJECT_NAME") ?? "stellaops";
values["COMPOSE_PROJECT_NAME"] = projectName;
break;
case RuntimeEnvironment.Kubernetes:
var ns = GetKubernetesNamespace() ?? "stellaops";
values["NAMESPACE"] = ns;
var kubeHost = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
if (kubeHost != null)
{
values["KUBERNETES_HOST"] = kubeHost;
}
break;
}
// Database defaults
values["DB_HOST"] = Environment.GetEnvironmentVariable("STELLAOPS_DB_HOST") ?? "localhost";
values["DB_PORT"] = Environment.GetEnvironmentVariable("STELLAOPS_DB_PORT") ?? "5432";
// Valkey defaults
values["VALKEY_HOST"] = Environment.GetEnvironmentVariable("STELLAOPS_VALKEY_HOST") ?? "localhost";
values["VALKEY_PORT"] = Environment.GetEnvironmentVariable("STELLAOPS_VALKEY_PORT") ?? "6379";
// Vault defaults
var vaultAddr = Environment.GetEnvironmentVariable("VAULT_ADDR");
if (vaultAddr != null)
{
values["VAULT_ADDR"] = vaultAddr;
}
return values;
}
private static int GetParentProcessId(int processId)
{
// Skip parent process detection - not reliable across platforms
// Windows Service detection is done via other signals
_ = processId; // Suppress unused parameter warning
return -1;
}
}

View File

@@ -0,0 +1,26 @@
namespace StellaOps.Doctor.Detection;
/// <summary>
/// The runtime environment where Stella Ops is deployed.
/// Used to generate runtime-specific remediation commands.
/// </summary>
public enum RuntimeEnvironment
{
/// <summary>Docker Compose deployment.</summary>
DockerCompose,
/// <summary>Kubernetes deployment.</summary>
Kubernetes,
/// <summary>systemd-managed services (Linux).</summary>
Systemd,
/// <summary>Windows Service deployment.</summary>
WindowsService,
/// <summary>Bare metal / manual deployment.</summary>
Bare,
/// <summary>Commands that work in any environment.</summary>
Any
}