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,189 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates cryptography provider licensing.
/// </summary>
public sealed class CryptoLicenseCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.license";
/// <inheritdoc />
public string Name => "Crypto Licensing";
/// <inheritdoc />
public string Description => "Validates licensed cryptography provider status and expiry";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "license", "compliance"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Only run if licensed providers are configured
var cryptoProEnabled = context.Configuration.GetValue<bool?>("Cryptography:CryptoPro:Enabled");
var licensedProviders = context.Configuration.GetSection("Cryptography:LicensedProviders").Get<string[]>();
return cryptoProEnabled == true || (licensedProviders?.Length > 0);
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
var licensedProviders = context.Configuration.GetSection("Cryptography:LicensedProviders").Get<string[]>()
?? [];
var issues = new List<string>();
var licenseInfo = new Dictionary<string, string>();
var checkedProviders = new List<string>();
// Check CryptoPro license
var cryptoProEnabled = context.Configuration.GetValue<bool?>("Cryptography:CryptoPro:Enabled");
if (cryptoProEnabled == true)
{
CheckCryptoProLicense(issues, licenseInfo, context.Configuration);
checkedProviders.Add("CryptoPro");
}
// Check other licensed providers
foreach (var provider in licensedProviders)
{
if (checkedProviders.Contains(provider, StringComparer.OrdinalIgnoreCase))
{
continue;
}
CheckProviderLicense(provider, issues, licenseInfo, context.Configuration);
checkedProviders.Add(provider);
}
licenseInfo["CheckedProviders"] = string.Join(", ", checkedProviders);
if (checkedProviders.Count == 0)
{
return Task.FromResult(result
.Info("No licensed cryptography providers configured")
.WithEvidence("License configuration", e =>
{
e.Add("LicensedProviders", "(none)");
})
.Build());
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} crypto license issue(s)")
.WithEvidence("License configuration", e =>
{
foreach (var kvp in licenseInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Verify license", "Check license file exists and is valid")
.AddManualStep(2, "Renew license", "Contact vendor to renew expired licenses")
.AddManualStep(3, "Configure license path", "Set Cryptography:<Provider>:LicensePath in configuration"))
.WithVerification("stella doctor --check check.crypto.license")
.Build());
}
return Task.FromResult(result
.Pass($"{checkedProviders.Count} crypto license(s) valid")
.WithEvidence("License configuration", e =>
{
foreach (var kvp in licenseInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.Build());
}
private static void CheckCryptoProLicense(List<string> issues, Dictionary<string, string> licenseInfo, IConfiguration config)
{
var licensePath = config.GetValue<string>("Cryptography:CryptoPro:LicensePath");
var licenseKey = config.GetValue<string>("Cryptography:CryptoPro:LicenseKey");
var expiryDate = config.GetValue<string>("Cryptography:CryptoPro:ExpiryDate");
licenseInfo["CryptoPro_LicenseConfigured"] = (!string.IsNullOrWhiteSpace(licensePath) || !string.IsNullOrWhiteSpace(licenseKey)).ToString();
if (!string.IsNullOrWhiteSpace(licensePath))
{
if (!File.Exists(licensePath))
{
issues.Add($"CryptoPro license file not found: {licensePath}");
}
licenseInfo["CryptoPro_LicensePath"] = licensePath;
}
if (!string.IsNullOrWhiteSpace(expiryDate))
{
if (DateTimeOffset.TryParse(expiryDate, CultureInfo.InvariantCulture, DateTimeStyles.None, out var expiry))
{
licenseInfo["CryptoPro_Expiry"] = expiry.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var daysUntilExpiry = (expiry - DateTimeOffset.UtcNow).TotalDays;
if (daysUntilExpiry < 0)
{
issues.Add("CryptoPro license has expired");
}
else if (daysUntilExpiry < 30)
{
issues.Add($"CryptoPro license expires in {daysUntilExpiry:F0} days");
}
}
}
}
private static void CheckProviderLicense(string provider, List<string> issues, Dictionary<string, string> licenseInfo, IConfiguration config)
{
var section = $"Cryptography:{provider}";
var licensePath = config.GetValue<string>($"{section}:LicensePath");
var expiryDate = config.GetValue<string>($"{section}:ExpiryDate");
var prefix = provider.Replace(" ", "_");
if (!string.IsNullOrWhiteSpace(licensePath))
{
licenseInfo[$"{prefix}_LicensePath"] = licensePath;
if (!File.Exists(licensePath))
{
issues.Add($"{provider} license file not found: {licensePath}");
}
}
if (!string.IsNullOrWhiteSpace(expiryDate))
{
if (DateTimeOffset.TryParse(expiryDate, CultureInfo.InvariantCulture, DateTimeStyles.None, out var expiry))
{
licenseInfo[$"{prefix}_Expiry"] = expiry.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var daysUntilExpiry = (expiry - DateTimeOffset.UtcNow).TotalDays;
if (daysUntilExpiry < 0)
{
issues.Add($"{provider} license has expired");
}
else if (daysUntilExpiry < 30)
{
issues.Add($"{provider} license expires in {daysUntilExpiry:F0} days");
}
}
}
}
}

View File

@@ -0,0 +1,172 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates CryptoPro CSP (Windows GOST provider) availability.
/// </summary>
public sealed class CryptoProCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.cryptopro";
/// <inheritdoc />
public string Name => "CryptoPro CSP";
/// <inheritdoc />
public string Description => "Validates CryptoPro CSP installation and configuration (Windows)";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "cryptopro", "gost", "windows", "regional"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(200);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Only run if CryptoPro is enabled or on Windows with GOST enabled
var cryptoProEnabled = context.Configuration.GetValue<bool?>("Cryptography:CryptoPro:Enabled");
if (cryptoProEnabled == true)
{
return true;
}
// Check if running on Windows with GOST enabled
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
var gostEnabled = context.Configuration.GetValue<bool?>("Cryptography:Gost:Enabled");
var gostProvider = context.Configuration.GetValue<string>("Cryptography:Gost:Provider");
return gostEnabled == true && gostProvider?.Equals("cryptopro", StringComparison.OrdinalIgnoreCase) == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Task.FromResult(result
.Skip("CryptoPro CSP is only available on Windows")
.WithEvidence("CryptoPro", e =>
{
e.Add("Platform", RuntimeInformation.OSDescription);
e.Add("Recommendation", "Use OpenSSL GOST engine on Linux");
})
.Build());
}
var issues = new List<string>();
var cryptoProInfo = new Dictionary<string, string>();
// Check build flag
var buildFlag = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_PRO");
cryptoProInfo["STELLAOPS_CRYPTO_PRO"] = buildFlag ?? "(not set)";
if (buildFlag != "1" && buildFlag?.ToLowerInvariant() != "true")
{
issues.Add("STELLAOPS_CRYPTO_PRO build flag not set - CryptoPro support may not be compiled in");
}
// Check installation paths
var installPaths = new[]
{
@"C:\Program Files\Crypto Pro\CSP",
@"C:\Program Files (x86)\Crypto Pro\CSP"
};
var foundPath = installPaths.FirstOrDefault(Directory.Exists);
if (foundPath != null)
{
cryptoProInfo["InstallPath"] = foundPath;
// Check for key executables
var csptest = Path.Combine(foundPath, "csptest.exe");
var cryptcp = Path.Combine(foundPath, "cryptcp.exe");
cryptoProInfo["CspTestExists"] = File.Exists(csptest).ToString();
cryptoProInfo["CryptcpExists"] = File.Exists(cryptcp).ToString();
// Try to detect version from files
var versionFile = Path.Combine(foundPath, "version.txt");
if (File.Exists(versionFile))
{
try
{
var version = File.ReadAllText(versionFile).Trim();
cryptoProInfo["Version"] = version;
}
catch
{
cryptoProInfo["Version"] = "(cannot read)";
}
}
}
else
{
issues.Add("CryptoPro CSP installation not found");
cryptoProInfo["InstallPath"] = "(not found)";
}
// Check for license
var licensePath = context.Configuration.GetValue<string>("Cryptography:CryptoPro:LicensePath");
if (!string.IsNullOrWhiteSpace(licensePath))
{
cryptoProInfo["LicensePath"] = licensePath;
if (!File.Exists(licensePath))
{
issues.Add($"CryptoPro license file not found: {licensePath}");
}
}
// Check for configured containers
var containerName = context.Configuration.GetValue<string>("Cryptography:CryptoPro:ContainerName");
if (!string.IsNullOrWhiteSpace(containerName))
{
cryptoProInfo["ContainerName"] = containerName;
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} CryptoPro issue(s)")
.WithEvidence("CryptoPro configuration", e =>
{
foreach (var kvp in cryptoProInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Install CryptoPro", "Download and install CryptoPro CSP from cryptopro.ru")
.AddManualStep(2, "Set build flag", "Set STELLAOPS_CRYPTO_PRO=1 environment variable")
.AddManualStep(3, "Configure license", "Configure Cryptography:CryptoPro:LicensePath"))
.WithVerification("stella doctor --check check.crypto.cryptopro")
.Build());
}
return Task.FromResult(result
.Pass("CryptoPro CSP is installed and configured")
.WithEvidence("CryptoPro configuration", e =>
{
foreach (var kvp in cryptoProInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.Build());
}
}

View File

@@ -0,0 +1,130 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates cryptography provider availability.
/// </summary>
public sealed class CryptoProviderAvailabilityCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.provider";
/// <inheritdoc />
public string Name => "Crypto Provider Availability";
/// <inheritdoc />
public string Description => "Validates cryptographic providers are available and properly configured";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "provider", "security"];
/// <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.cryptography", DoctorCategory.Cryptography.ToString());
var configuredProviders = context.Configuration.GetSection("Cryptography:Providers").Get<string[]>()
?? ["default"];
var defaultAlgorithm = context.Configuration.GetValue<string>("Cryptography:DefaultAlgorithm")
?? "ecdsa-p256";
var issues = new List<string>();
var availableProviders = new List<string>();
// Check default .NET crypto providers
try
{
using var ecdsa = System.Security.Cryptography.ECDsa.Create();
if (ecdsa != null)
{
availableProviders.Add("ECDSA (P-256/P-384/P-521)");
}
}
catch
{
issues.Add("ECDSA provider not available");
}
try
{
using var rsa = System.Security.Cryptography.RSA.Create();
if (rsa != null)
{
availableProviders.Add("RSA (2048/4096)");
}
}
catch
{
issues.Add("RSA provider not available");
}
try
{
using var aes = System.Security.Cryptography.Aes.Create();
if (aes != null)
{
availableProviders.Add("AES (128/256)");
}
}
catch
{
issues.Add("AES provider not available");
}
// Check EdDSA if configured
if (configuredProviders.Contains("ed25519", StringComparer.OrdinalIgnoreCase))
{
try
{
// Ed25519 is available in .NET 9+
availableProviders.Add("Ed25519");
}
catch
{
issues.Add("Ed25519 provider not available");
}
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} crypto provider issue(s)")
.WithEvidence("Crypto providers", e =>
{
e.Add("ConfiguredProviders", string.Join(", ", configuredProviders));
e.Add("AvailableProviders", string.Join(", ", availableProviders));
e.Add("DefaultAlgorithm", defaultAlgorithm);
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Check runtime", "Ensure .NET runtime supports required algorithms")
.AddManualStep(2, "Install providers", "Install additional crypto libraries if needed"))
.WithVerification("stella doctor --check check.crypto.provider")
.Build());
}
return Task.FromResult(result
.Pass($"{availableProviders.Count} crypto provider(s) available")
.WithEvidence("Crypto providers", e =>
{
e.Add("ConfiguredProviders", string.Join(", ", configuredProviders));
e.Add("AvailableProviders", string.Join(", ", availableProviders));
e.Add("DefaultAlgorithm", defaultAlgorithm);
})
.Build());
}
}

View File

@@ -0,0 +1,200 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates eIDAS (EU qualified signatures) provider configuration.
/// </summary>
public sealed class EidasProviderCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.eidas";
/// <inheritdoc />
public string Name => "eIDAS Provider";
/// <inheritdoc />
public string Description => "Validates eIDAS qualified signature provider configuration";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "eidas", "qualified", "eu", "regional"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Only run if eIDAS is configured or enabled
var eidasEnabled = context.Configuration.GetValue<bool?>("Cryptography:Eidas:Enabled")
?? context.Configuration.GetValue<bool?>("Cryptography:EnableEidas");
return eidasEnabled == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
var eidasProvider = context.Configuration.GetValue<string>("Cryptography:Eidas:Provider")
?? "pkcs11";
var trustListUrl = context.Configuration.GetValue<string>("Cryptography:Eidas:TrustListUrl")
?? "https://ec.europa.eu/tools/lotl/eu-lotl.xml";
var issues = new List<string>();
var providerInfo = new Dictionary<string, string>
{
["ConfiguredProvider"] = eidasProvider,
["TrustListUrl"] = trustListUrl
};
switch (eidasProvider.ToLowerInvariant())
{
case "pkcs11":
CheckPkcs11Eidas(issues, providerInfo, context.Configuration);
break;
case "certificate":
CheckCertificateEidas(issues, providerInfo, context.Configuration);
break;
case "remote":
CheckRemoteEidas(issues, providerInfo, context.Configuration);
break;
default:
issues.Add($"Unknown eIDAS provider: {eidasProvider}");
break;
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} eIDAS provider issue(s)")
.WithEvidence("eIDAS configuration", e =>
{
foreach (var kvp in providerInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Configure provider", "Configure PKCS#11 library or certificate store for eIDAS")
.AddManualStep(2, "Verify trust list", "Ensure EU Trust List is accessible"))
.WithVerification("stella doctor --check check.crypto.eidas")
.Build());
}
return Task.FromResult(result
.Pass("eIDAS provider is configured")
.WithEvidence("eIDAS configuration", e =>
{
foreach (var kvp in providerInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.Build());
}
private static void CheckPkcs11Eidas(List<string> issues, Dictionary<string, string> providerInfo, IConfiguration config)
{
providerInfo["Provider"] = "PKCS#11 (Smart Card/HSM)";
var pkcs11Library = config.GetValue<string>("Cryptography:Eidas:Pkcs11Library");
if (string.IsNullOrWhiteSpace(pkcs11Library))
{
issues.Add("PKCS#11 library path not configured for eIDAS");
providerInfo["Library"] = "(not set)";
}
else if (!File.Exists(pkcs11Library))
{
issues.Add($"PKCS#11 library not found: {pkcs11Library}");
providerInfo["Library"] = pkcs11Library;
}
else
{
providerInfo["Library"] = pkcs11Library;
}
var slotId = config.GetValue<int?>("Cryptography:Eidas:SlotId");
providerInfo["SlotId"] = slotId?.ToString() ?? "(auto-detect)";
}
private static void CheckCertificateEidas(List<string> issues, Dictionary<string, string> providerInfo, IConfiguration config)
{
providerInfo["Provider"] = "Certificate Store";
var certThumbprint = config.GetValue<string>("Cryptography:Eidas:CertificateThumbprint");
var certPath = config.GetValue<string>("Cryptography:Eidas:CertificatePath");
if (!string.IsNullOrWhiteSpace(certThumbprint))
{
providerInfo["CertificateThumbprint"] = certThumbprint;
// Try to find certificate in store
try
{
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(
X509FindType.FindByThumbprint,
certThumbprint,
validOnly: false);
if (certs.Count == 0)
{
issues.Add($"Certificate with thumbprint {certThumbprint[..8]}... not found in store");
}
else
{
var cert = certs[0];
providerInfo["CertificateSubject"] = cert.Subject;
providerInfo["CertificateExpiry"] = cert.NotAfter.ToString("yyyy-MM-dd");
}
}
catch (Exception ex)
{
issues.Add($"Cannot access certificate store: {ex.Message}");
}
}
else if (!string.IsNullOrWhiteSpace(certPath))
{
providerInfo["CertificatePath"] = certPath;
if (!File.Exists(certPath))
{
issues.Add($"Certificate file not found: {certPath}");
}
}
else
{
issues.Add("No certificate thumbprint or path configured for eIDAS");
}
}
private static void CheckRemoteEidas(List<string> issues, Dictionary<string, string> providerInfo, IConfiguration config)
{
providerInfo["Provider"] = "Remote Signing Service";
var remoteEndpoint = config.GetValue<string>("Cryptography:Eidas:RemoteEndpoint");
if (string.IsNullOrWhiteSpace(remoteEndpoint))
{
issues.Add("Remote eIDAS signing endpoint not configured");
providerInfo["Endpoint"] = "(not set)";
}
else
{
providerInfo["Endpoint"] = remoteEndpoint;
}
}
}

View File

@@ -0,0 +1,147 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates FIPS 140-2/140-3 compliance mode.
/// </summary>
public sealed class FipsComplianceCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.fips";
/// <inheritdoc />
public string Name => "FIPS Compliance";
/// <inheritdoc />
public string Description => "Validates FIPS 140-2/140-3 compliance mode is configured correctly";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "fips", "compliance", "security"];
/// <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.cryptography", DoctorCategory.Cryptography.ToString());
var fipsRequired = context.Configuration.GetValue<bool?>("Cryptography:RequireFips")
?? context.Configuration.GetValue<bool?>("Security:FipsMode");
var fipsEnabled = IsFipsModeEnabled();
if (fipsRequired == true && !fipsEnabled)
{
return Task.FromResult(result
.Fail("FIPS mode required but not enabled")
.WithEvidence("FIPS configuration", e =>
{
e.Add("FipsRequired", "true");
e.Add("FipsEnabled", "false");
e.Add("Platform", RuntimeInformation.OSDescription);
})
.WithCauses("System FIPS mode is not enabled but configuration requires it")
.WithRemediation(r => r
.AddManualStep(1, "Enable FIPS on Windows", "Set FIPS security policy in Windows Group Policy")
.AddManualStep(2, "Enable FIPS on Linux", "Configure system crypto policy with fips-mode-setup"))
.WithVerification("stella doctor --check check.crypto.fips")
.Build());
}
if (fipsRequired == false && fipsEnabled)
{
return Task.FromResult(result
.Info("FIPS mode enabled but not required by configuration")
.WithEvidence("FIPS configuration", e =>
{
e.Add("FipsRequired", "false");
e.Add("FipsEnabled", "true");
e.Add("Platform", RuntimeInformation.OSDescription);
e.Add("Note", "Running in FIPS mode may restrict some algorithms");
})
.Build());
}
if (fipsRequired == null)
{
return Task.FromResult(result
.Info($"FIPS mode: {(fipsEnabled ? "enabled" : "disabled")} (not explicitly configured)")
.WithEvidence("FIPS configuration", e =>
{
e.Add("FipsRequired", "(not set)");
e.Add("FipsEnabled", fipsEnabled.ToString());
e.Add("Platform", RuntimeInformation.OSDescription);
})
.Build());
}
return Task.FromResult(result
.Pass("FIPS compliance matches requirements")
.WithEvidence("FIPS configuration", e =>
{
e.Add("FipsRequired", fipsRequired.ToString()!);
e.Add("FipsEnabled", fipsEnabled.ToString());
e.Add("Platform", RuntimeInformation.OSDescription);
})
.Build());
}
private static bool IsFipsModeEnabled()
{
try
{
// Check environment variable first
var envFips = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_SECURITY_CRYPTOGRAPHY_USELEGACYMACOPENSSL");
if (envFips == "0")
{
return false;
}
// On Windows, check registry (simplified check)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Try to detect FIPS via CryptoConfig
try
{
// In FIPS mode, certain algorithms will be unavailable
using var md5 = System.Security.Cryptography.MD5.Create();
// If MD5 works, FIPS is likely not enforced
return false;
}
catch (System.Security.Cryptography.CryptographicException)
{
// MD5 blocked - FIPS mode likely enabled
return true;
}
}
// On Linux, check crypto policies
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var policyFile = "/etc/crypto-policies/state/current";
if (File.Exists(policyFile))
{
var policy = File.ReadAllText(policyFile).Trim();
return policy.Contains("FIPS", StringComparison.OrdinalIgnoreCase);
}
}
return false;
}
catch
{
return false;
}
}
}

View File

@@ -0,0 +1,209 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates GOST (Russian) cryptography provider availability.
/// </summary>
public sealed class GostProviderCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.gost";
/// <inheritdoc />
public string Name => "GOST Cryptography";
/// <inheritdoc />
public string Description => "Validates GOST R 34.10-2012 / R 34.11-2012 cryptography provider";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "gost", "regional", "russia"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Only run if GOST is configured or enabled
var gostEnabled = context.Configuration.GetValue<bool?>("Cryptography:Gost:Enabled")
?? context.Configuration.GetValue<bool?>("Cryptography:EnableGost");
return gostEnabled == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
var gostProvider = context.Configuration.GetValue<string>("Cryptography:Gost:Provider")
?? "openssl-gost";
var gostEndpoint = context.Configuration.GetValue<string>("Cryptography:Gost:Endpoint");
var issues = new List<string>();
var providerInfo = new Dictionary<string, string>();
// Check which GOST provider is configured
switch (gostProvider.ToLowerInvariant())
{
case "openssl-gost":
CheckOpenSslGost(issues, providerInfo);
break;
case "cryptopro":
CheckCryptoProGost(issues, providerInfo);
break;
case "pkcs11-gost":
CheckPkcs11Gost(issues, providerInfo, context.Configuration);
break;
case "remote":
if (string.IsNullOrWhiteSpace(gostEndpoint))
{
issues.Add("Remote GOST provider configured but no endpoint specified");
}
providerInfo["Provider"] = "Remote Service";
providerInfo["Endpoint"] = gostEndpoint ?? "(not set)";
break;
default:
issues.Add($"Unknown GOST provider: {gostProvider}");
break;
}
providerInfo["ConfiguredProvider"] = gostProvider;
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} GOST provider issue(s)")
.WithEvidence("GOST cryptography", e =>
{
foreach (var kvp in providerInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Install provider", "Install the configured GOST provider (OpenSSL GOST engine, CryptoPro CSP, or PKCS#11)")
.AddManualStep(2, "Configure endpoint", "For remote providers, configure the service endpoint"))
.WithVerification("stella doctor --check check.crypto.gost")
.Build());
}
return Task.FromResult(result
.Pass("GOST cryptography provider is configured")
.WithEvidence("GOST cryptography", e =>
{
foreach (var kvp in providerInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.Build());
}
private static void CheckOpenSslGost(List<string> issues, Dictionary<string, string> providerInfo)
{
providerInfo["Provider"] = "OpenSSL GOST Engine";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Check for GOST engine in common locations
var enginePaths = new[]
{
"/usr/lib/x86_64-linux-gnu/engines-3/gost.so",
"/usr/lib64/engines-3/gost.so",
"/usr/lib/engines/gost.so"
};
var foundEngine = enginePaths.FirstOrDefault(File.Exists);
if (foundEngine != null)
{
providerInfo["EnginePath"] = foundEngine;
}
else
{
issues.Add("OpenSSL GOST engine not found in standard locations");
providerInfo["EnginePath"] = "(not found)";
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
issues.Add("OpenSSL GOST engine is primarily supported on Linux");
providerInfo["Note"] = "Consider using CryptoPro CSP on Windows";
}
}
private static void CheckCryptoProGost(List<string> issues, Dictionary<string, string> providerInfo)
{
providerInfo["Provider"] = "CryptoPro CSP";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Check for CryptoPro installation
var cryptoProPaths = new[]
{
@"C:\Program Files\Crypto Pro\CSP",
@"C:\Program Files (x86)\Crypto Pro\CSP"
};
var foundPath = cryptoProPaths.FirstOrDefault(Directory.Exists);
if (foundPath != null)
{
providerInfo["InstallPath"] = foundPath;
}
else
{
issues.Add("CryptoPro CSP not found");
providerInfo["InstallPath"] = "(not found)";
}
// Check build flag
var buildFlag = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_PRO");
providerInfo["BuildFlag"] = buildFlag ?? "(not set)";
if (buildFlag != "1" && buildFlag?.ToLowerInvariant() != "true")
{
issues.Add("STELLAOPS_CRYPTO_PRO build flag not set");
}
}
else
{
issues.Add("CryptoPro CSP is only supported on Windows");
}
}
private static void CheckPkcs11Gost(List<string> issues, Dictionary<string, string> providerInfo, Microsoft.Extensions.Configuration.IConfiguration config)
{
providerInfo["Provider"] = "PKCS#11 GOST Token";
var pkcs11Library = config.GetValue<string>("Cryptography:Gost:Pkcs11Library");
if (string.IsNullOrWhiteSpace(pkcs11Library))
{
issues.Add("PKCS#11 library path not configured");
providerInfo["Library"] = "(not set)";
}
else if (!File.Exists(pkcs11Library))
{
issues.Add($"PKCS#11 library not found: {pkcs11Library}");
providerInfo["Library"] = pkcs11Library;
}
else
{
providerInfo["Library"] = pkcs11Library;
}
var slotId = config.GetValue<int?>("Cryptography:Gost:SlotId");
providerInfo["SlotId"] = slotId?.ToString() ?? "0";
}
}

View File

@@ -0,0 +1,265 @@
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates HSM (Hardware Security Module) connectivity.
/// </summary>
public sealed class HsmConnectivityCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.hsm";
/// <inheritdoc />
public string Name => "HSM Connectivity";
/// <inheritdoc />
public string Description => "Validates HSM/PKCS#11 hardware token connectivity";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "hsm", "pkcs11", "hardware", "security"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Only run if HSM is configured
var hsmEnabled = context.Configuration.GetValue<bool?>("Cryptography:Hsm:Enabled")
?? context.Configuration.GetValue<bool?>("Cryptography:EnableHsm");
return hsmEnabled == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
var hsmType = context.Configuration.GetValue<string>("Cryptography:Hsm:Type")
?? "pkcs11";
var pkcs11Library = context.Configuration.GetValue<string>("Cryptography:Hsm:Pkcs11Library")
?? context.Configuration.GetValue<string>("Cryptography:Pkcs11:Library");
var slotId = context.Configuration.GetValue<int?>("Cryptography:Hsm:SlotId")
?? context.Configuration.GetValue<int?>("Cryptography:Pkcs11:SlotId")
?? 0;
var issues = new List<string>();
var hsmInfo = new Dictionary<string, string>
{
["HsmType"] = hsmType,
["SlotId"] = slotId.ToString()
};
switch (hsmType.ToLowerInvariant())
{
case "pkcs11":
CheckPkcs11Hsm(issues, hsmInfo, pkcs11Library);
break;
case "softhsm":
CheckSoftHsm(issues, hsmInfo);
break;
case "azure":
case "azurekeyvault":
CheckAzureKeyVault(issues, hsmInfo, context.Configuration);
break;
case "aws":
case "awskms":
CheckAwsKms(issues, hsmInfo, context.Configuration);
break;
case "gcp":
case "gcpkms":
CheckGcpKms(issues, hsmInfo, context.Configuration);
break;
case "hashicorp":
case "vault":
CheckHashiCorpVault(issues, hsmInfo, context.Configuration);
break;
default:
issues.Add($"Unknown HSM type: {hsmType}");
break;
}
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} HSM connectivity issue(s)")
.WithEvidence("HSM configuration", e =>
{
foreach (var kvp in hsmInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Install PKCS#11 library", "Ensure the HSM PKCS#11 library is installed")
.AddManualStep(2, "Check connectivity", "Verify network/USB connectivity to HSM")
.AddManualStep(3, "Verify credentials", "Ensure HSM credentials are configured"))
.WithVerification("stella doctor --check check.crypto.hsm")
.Build());
}
return Task.FromResult(result
.Pass("HSM is accessible")
.WithEvidence("HSM configuration", e =>
{
foreach (var kvp in hsmInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.Build());
}
private static void CheckPkcs11Hsm(List<string> issues, Dictionary<string, string> hsmInfo, string? pkcs11Library)
{
hsmInfo["Provider"] = "PKCS#11";
if (string.IsNullOrWhiteSpace(pkcs11Library))
{
issues.Add("PKCS#11 library path not configured");
hsmInfo["Library"] = "(not set)";
return;
}
hsmInfo["Library"] = pkcs11Library;
if (!File.Exists(pkcs11Library))
{
issues.Add($"PKCS#11 library not found: {pkcs11Library}");
return;
}
// Library exists - basic check passed
hsmInfo["LibraryExists"] = "true";
}
private static void CheckSoftHsm(List<string> issues, Dictionary<string, string> hsmInfo)
{
hsmInfo["Provider"] = "SoftHSM";
// Check common SoftHSM library locations
var libraryPaths = new[]
{
"/usr/lib/softhsm/libsofthsm2.so",
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
"/usr/local/lib/softhsm/libsofthsm2.so"
};
var foundLibrary = libraryPaths.FirstOrDefault(File.Exists);
if (foundLibrary != null)
{
hsmInfo["Library"] = foundLibrary;
}
else
{
issues.Add("SoftHSM library not found in standard locations");
hsmInfo["Library"] = "(not found)";
}
// Check for token directory
var tokenDir = Environment.GetEnvironmentVariable("SOFTHSM2_CONF");
hsmInfo["ConfigPath"] = tokenDir ?? "(using default)";
}
private static void CheckAzureKeyVault(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
{
hsmInfo["Provider"] = "Azure Key Vault";
var vaultUrl = config.GetValue<string>("Cryptography:Hsm:Azure:VaultUrl")
?? config.GetValue<string>("AzureKeyVault:VaultUrl");
if (string.IsNullOrWhiteSpace(vaultUrl))
{
issues.Add("Azure Key Vault URL not configured");
hsmInfo["VaultUrl"] = "(not set)";
}
else
{
hsmInfo["VaultUrl"] = vaultUrl;
}
// Check for credential configuration
var tenantId = config.GetValue<string>("Cryptography:Hsm:Azure:TenantId")
?? config.GetValue<string>("AzureKeyVault:TenantId");
hsmInfo["TenantConfigured"] = (!string.IsNullOrWhiteSpace(tenantId)).ToString();
}
private static void CheckAwsKms(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
{
hsmInfo["Provider"] = "AWS KMS";
var region = config.GetValue<string>("Cryptography:Hsm:Aws:Region")
?? config.GetValue<string>("AWS:Region");
if (string.IsNullOrWhiteSpace(region))
{
issues.Add("AWS region not configured");
hsmInfo["Region"] = "(not set)";
}
else
{
hsmInfo["Region"] = region;
}
var keyId = config.GetValue<string>("Cryptography:Hsm:Aws:KeyId");
hsmInfo["KeyConfigured"] = (!string.IsNullOrWhiteSpace(keyId)).ToString();
}
private static void CheckGcpKms(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
{
hsmInfo["Provider"] = "Google Cloud KMS";
var projectId = config.GetValue<string>("Cryptography:Hsm:Gcp:ProjectId")
?? config.GetValue<string>("GCP:ProjectId");
if (string.IsNullOrWhiteSpace(projectId))
{
issues.Add("GCP project ID not configured");
hsmInfo["ProjectId"] = "(not set)";
}
else
{
hsmInfo["ProjectId"] = projectId;
}
var keyRing = config.GetValue<string>("Cryptography:Hsm:Gcp:KeyRing");
hsmInfo["KeyRingConfigured"] = (!string.IsNullOrWhiteSpace(keyRing)).ToString();
}
private static void CheckHashiCorpVault(List<string> issues, Dictionary<string, string> hsmInfo, IConfiguration config)
{
hsmInfo["Provider"] = "HashiCorp Vault";
var vaultAddr = config.GetValue<string>("Cryptography:Hsm:Vault:Address")
?? Environment.GetEnvironmentVariable("VAULT_ADDR");
if (string.IsNullOrWhiteSpace(vaultAddr))
{
issues.Add("HashiCorp Vault address not configured");
hsmInfo["Address"] = "(not set)";
}
else
{
hsmInfo["Address"] = vaultAddr;
}
var transitPath = config.GetValue<string>("Cryptography:Hsm:Vault:TransitPath") ?? "transit";
hsmInfo["TransitPath"] = transitPath;
}
}

View File

@@ -0,0 +1,160 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.Checks;
/// <summary>
/// Validates SM2/SM3/SM4 (Chinese) cryptography provider availability.
/// </summary>
public sealed class SmProviderCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.crypto.sm";
/// <inheritdoc />
public string Name => "SM Cryptography";
/// <inheritdoc />
public string Description => "Validates SM2/SM3/SM4 (GB/T) cryptography provider";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["cryptography", "sm2", "sm3", "sm4", "regional", "china"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
// Only run if SM crypto is configured or enabled
var smEnabled = context.Configuration.GetValue<bool?>("Cryptography:Sm:Enabled")
?? context.Configuration.GetValue<bool?>("Cryptography:EnableSm");
return smEnabled == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.cryptography", DoctorCategory.Cryptography.ToString());
var smProvider = context.Configuration.GetValue<string>("Cryptography:Sm:Provider")
?? "smsoft";
var smEndpoint = context.Configuration.GetValue<string>("Cryptography:Sm:Endpoint")
?? context.Configuration.GetValue<string>("SmRemote:Endpoint");
var issues = new List<string>();
var providerInfo = new Dictionary<string, string>();
// Check environment gate for SM providers
var smSoftAllowed = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
providerInfo["SM_SOFT_ALLOWED"] = smSoftAllowed ?? "(not set)";
switch (smProvider.ToLowerInvariant())
{
case "smsoft":
CheckSmSoft(issues, providerInfo);
break;
case "smremote":
case "remote":
CheckSmRemote(issues, providerInfo, smEndpoint);
break;
case "bouncycastle":
CheckBouncyCastleSm(issues, providerInfo);
break;
default:
issues.Add($"Unknown SM provider: {smProvider}");
break;
}
providerInfo["ConfiguredProvider"] = smProvider;
if (issues.Count > 0)
{
return Task.FromResult(result
.Warn($"{issues.Count} SM provider issue(s)")
.WithEvidence("SM cryptography", e =>
{
foreach (var kvp in providerInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.WithCauses(issues.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Set environment gate", "Set SM_SOFT_ALLOWED=1 to enable SM software providers")
.AddManualStep(2, "Configure SmRemote", "Configure SmRemote:Endpoint for remote SM crypto service"))
.WithVerification("stella doctor --check check.crypto.sm")
.Build());
}
return Task.FromResult(result
.Pass("SM cryptography provider is configured")
.WithEvidence("SM cryptography", e =>
{
foreach (var kvp in providerInfo)
{
e.Add(kvp.Key, kvp.Value);
}
})
.Build());
}
private static void CheckSmSoft(List<string> issues, Dictionary<string, string> providerInfo)
{
providerInfo["Provider"] = "SmSoft (Software Implementation)";
var smSoftAllowed = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
if (smSoftAllowed != "1" && smSoftAllowed?.ToLowerInvariant() != "true")
{
issues.Add("SM_SOFT_ALLOWED environment variable not set - SmSoft provider may not be available");
}
// Check if BouncyCastle is available for SM algorithms
providerInfo["Implementation"] = "BouncyCastle (managed)";
}
private static void CheckSmRemote(List<string> issues, Dictionary<string, string> providerInfo, string? endpoint)
{
providerInfo["Provider"] = "SmRemote (Remote Service)";
if (string.IsNullOrWhiteSpace(endpoint))
{
issues.Add("SmRemote endpoint not configured");
providerInfo["Endpoint"] = "(not set)";
}
else
{
providerInfo["Endpoint"] = endpoint;
// Check if endpoint looks valid
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
{
issues.Add($"SmRemote endpoint is not a valid URI: {endpoint}");
}
else
{
providerInfo["Host"] = uri.Host;
providerInfo["Port"] = uri.Port.ToString();
}
}
}
private static void CheckBouncyCastleSm(List<string> issues, Dictionary<string, string> providerInfo)
{
providerInfo["Provider"] = "BouncyCastle";
providerInfo["Implementation"] = "Managed .NET implementation";
// BouncyCastle should be available if the package is referenced
providerInfo["Algorithms"] = "SM2, SM3, SM4";
}
}

View File

@@ -0,0 +1,46 @@
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Cryptography.Checks;
namespace StellaOps.Doctor.Plugins.Cryptography;
/// <summary>
/// Plugin providing cryptography diagnostic checks including regional crypto,
/// HSM connectivity, and licensing validation.
/// </summary>
public sealed class CryptographyPlugin : IDoctorPlugin
{
/// <inheritdoc />
public string PluginId => "stellaops.doctor.cryptography";
/// <inheritdoc />
public string DisplayName => "Cryptography";
/// <inheritdoc />
public DoctorCategory Category => DoctorCategory.Cryptography;
/// <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 CryptoProviderAvailabilityCheck(),
new FipsComplianceCheck(),
new GostProviderCheck(),
new SmProviderCheck(),
new EidasProviderCheck(),
new HsmConnectivityCheck(),
new CryptoLicenseCheck(),
new CryptoProCheck()
];
/// <inheritdoc />
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
}

View File

@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Cryptography.DependencyInjection;
/// <summary>
/// Extension methods for registering the Cryptography diagnostics plugin.
/// </summary>
public static class CryptographyPluginExtensions
{
/// <summary>
/// Adds the Cryptography diagnostics plugin to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddDoctorCryptographyPlugin(this IServiceCollection services)
{
services.AddSingleton<IDoctorPlugin, CryptographyPlugin>();
return services;
}
}

View File

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