audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration

This commit is contained in:
master
2026-01-14 10:48:00 +02:00
parent d7be6ba34b
commit 95d5898650
379 changed files with 40695 additions and 19041 deletions

View File

@@ -0,0 +1,172 @@
// -----------------------------------------------------------------------------
// MirrorServerAuthCheck.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: Check for mirror server authentication configuration
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using StellaOps.Concelier.Core.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Sources.Checks;
/// <summary>
/// Verifies mirror server authentication is properly configured.
/// </summary>
public sealed class MirrorServerAuthCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.sources.mirror.auth";
/// <inheritdoc />
public string Name => "Mirror Server Authentication";
/// <inheritdoc />
public string Description => "Verifies mirror server authentication configuration when OAuth is enabled";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["security", "sources", "mirror", "authentication"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var config = context.Configuration
.GetSection("sources:mirrorServer")
.Get<MirrorServerConfig>();
// Only run if mirror server is enabled
return config?.Enabled == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
var config = context.Configuration
.GetSection("sources:mirrorServer")
.Get<MirrorServerConfig>();
if (config is null || !config.Enabled)
{
return Task.FromResult(result
.Skip("Mirror server not enabled")
.WithEvidence("Mirror server", e => e.Add("Enabled", "false"))
.Build());
}
// Check authentication mode
if (config.Authentication == MirrorAuthMode.Anonymous)
{
return Task.FromResult(result
.Info("Mirror server configured for anonymous access")
.WithEvidence("Mirror server authentication", e =>
{
e.Add("AuthMode", "Anonymous");
e.Add("ExportRoot", config.ExportRoot);
})
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
// OAuth mode - validate configuration
if (config.Authentication == MirrorAuthMode.OAuth)
{
if (config.OAuth is null)
{
return Task.FromResult(result
.Fail("OAuth authentication enabled but OAuth configuration missing")
.WithEvidence("Mirror server authentication", e =>
{
e.Add("AuthMode", "OAuth");
e.Add("OAuthConfigured", "false");
})
.WithCauses(
"OAuth section not configured in mirror server settings",
"Missing required OAuth issuer configuration")
.WithRemediation(r => r
.AddManualStep(1, "Configure OAuth settings", "Add 'sources:mirrorServer:oauth' section with issuer URL")
.AddShellStep(2, "Run setup wizard", "stella setup --step sources"))
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
// Validate OAuth configuration - Issuer is required
if (string.IsNullOrWhiteSpace(config.OAuth.Issuer))
{
return Task.FromResult(result
.Fail("OAuth configuration incomplete: missing Issuer")
.WithEvidence("Mirror server OAuth", e =>
{
e.Add("AuthMode", "OAuth");
e.Add("IssuerConfigured", "false");
})
.WithCauses("OAuth Issuer URL not configured")
.WithRemediation(r => r
.AddManualStep(1, "Configure OAuth issuer", "Set 'sources:mirrorServer:oauth:issuer' to your OIDC provider URL")
.AddShellStep(2, "Verify issuer metadata", "curl -s {issuer}/.well-known/openid-configuration"))
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
return Task.FromResult(result
.Pass("Mirror server OAuth authentication configured")
.WithEvidence("Mirror server OAuth", e =>
{
e.Add("AuthMode", "OAuth");
e.Add("Issuer", config.OAuth.Issuer);
e.Add("Audience", config.OAuth.Audience ?? "(not set)");
e.Add("RequiredScopesCount", config.OAuth.RequiredScopes.Length.ToString());
e.Add("RequireHttpsMetadata", config.OAuth.RequireHttpsMetadata.ToString());
})
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
// ApiKey mode
if (config.Authentication == MirrorAuthMode.ApiKey)
{
return Task.FromResult(result
.Pass("Mirror server configured with API key authentication")
.WithEvidence("Mirror server authentication", e =>
{
e.Add("AuthMode", "ApiKey");
e.Add("ExportRoot", config.ExportRoot);
})
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
// mTLS mode
if (config.Authentication == MirrorAuthMode.Mtls)
{
return Task.FromResult(result
.Pass("Mirror server configured with mTLS authentication")
.WithEvidence("Mirror server authentication", e =>
{
e.Add("AuthMode", "Mtls");
e.Add("ExportRoot", config.ExportRoot);
})
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
return Task.FromResult(result
.Pass($"Mirror server authentication mode: {config.Authentication}")
.WithEvidence("Mirror server authentication", e =>
{
e.Add("AuthMode", config.Authentication.ToString());
e.Add("ExportRoot", config.ExportRoot);
})
.WithVerification("stella doctor --check check.sources.mirror.auth")
.Build());
}
}

View File

@@ -0,0 +1,174 @@
// -----------------------------------------------------------------------------
// MirrorServerRateLimitCheck.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: Check for mirror server rate limiting configuration
// -----------------------------------------------------------------------------
using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Concelier.Core.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Sources.Checks;
/// <summary>
/// Verifies mirror server rate limiting is properly configured.
/// </summary>
public sealed class MirrorServerRateLimitCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.sources.mirror.ratelimit";
/// <inheritdoc />
public string Name => "Mirror Server Rate Limiting";
/// <inheritdoc />
public string Description => "Verifies mirror server rate limiting configuration for Router integration";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["configuration", "sources", "mirror", "ratelimit"];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var config = context.Configuration
.GetSection("sources:mirrorServer")
.Get<MirrorServerConfig>();
// Only run if mirror server is enabled
return config?.Enabled == true;
}
/// <inheritdoc />
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
var config = context.Configuration
.GetSection("sources:mirrorServer")
.Get<MirrorServerConfig>();
if (config is null || !config.Enabled)
{
return Task.FromResult(result
.Skip("Mirror server not enabled")
.WithEvidence("Mirror server", e => e.Add("Enabled", "false"))
.Build());
}
var rateLimits = config.RateLimits;
if (!rateLimits.IsEnabled)
{
return Task.FromResult(result
.Info("Rate limiting not enabled for mirror server")
.WithEvidence("Rate limiting", e =>
{
e.Add("Enabled", "false");
e.Add("Recommendation", "Consider enabling rate limiting to protect against abuse");
})
.WithVerification("stella doctor --check check.sources.mirror.ratelimit")
.Build());
}
// Validate configuration
var warnings = new List<string>();
// Check instance-level limits
if (rateLimits.ForInstance is null)
{
warnings.Add("Instance-level rate limiting not configured");
}
else
{
if (rateLimits.ForInstance.MaxRequests <= 0)
warnings.Add("Instance MaxRequests should be positive");
if (rateLimits.ForInstance.PerSeconds <= 0)
warnings.Add("Instance PerSeconds should be positive");
}
// Check environment-level limits
if (rateLimits.ForEnvironment is not null)
{
if (string.IsNullOrWhiteSpace(rateLimits.ForEnvironment.ValkeyConnection))
{
warnings.Add("Environment rate limiting configured but Valkey connection missing");
}
if (rateLimits.ForEnvironment.MaxRequests <= 0)
warnings.Add("Environment MaxRequests should be positive");
if (rateLimits.ForEnvironment.PerSeconds <= 0)
warnings.Add("Environment PerSeconds should be positive");
}
if (warnings.Count > 0)
{
return Task.FromResult(result
.Warn($"Rate limiting has configuration issues: {warnings.Count} warning(s)")
.WithEvidence("Rate limiting configuration", e =>
{
e.Add("Enabled", "true");
e.Add("Warnings", string.Join("; ", warnings));
if (rateLimits.ForInstance is not null)
{
e.Add("Instance.MaxRequests", rateLimits.ForInstance.MaxRequests.ToString(CultureInfo.InvariantCulture));
e.Add("Instance.PerSeconds", rateLimits.ForInstance.PerSeconds.ToString(CultureInfo.InvariantCulture));
}
if (rateLimits.ForEnvironment is not null)
{
e.Add("Environment.MaxRequests", rateLimits.ForEnvironment.MaxRequests.ToString(CultureInfo.InvariantCulture));
e.Add("Environment.PerSeconds", rateLimits.ForEnvironment.PerSeconds.ToString(CultureInfo.InvariantCulture));
e.Add("Environment.ValkeyConfigured", (!string.IsNullOrWhiteSpace(rateLimits.ForEnvironment.ValkeyConnection)).ToString());
}
})
.WithCauses(warnings.ToArray())
.WithRemediation(r => r
.AddManualStep(1, "Review rate limit configuration", "Check sources:mirrorServer:rateLimits in configuration")
.AddManualStep(2, "Set appropriate limits", "Configure MaxRequests and PerSeconds for your expected traffic"))
.WithVerification("stella doctor --check check.sources.mirror.ratelimit")
.Build());
}
// Build route summary
var routeCount = rateLimits.ForEnvironment?.Routes.Count ?? 0;
return Task.FromResult(result
.Pass($"Rate limiting properly configured with {routeCount} route-specific rule(s)")
.WithEvidence("Rate limiting configuration", e =>
{
e.Add("Enabled", "true");
e.Add("ActivationThresholdPer5Min", rateLimits.ActivationThresholdPer5Min.ToString(CultureInfo.InvariantCulture));
if (rateLimits.ForInstance is not null)
{
e.Add("Instance.MaxRequests", rateLimits.ForInstance.MaxRequests.ToString(CultureInfo.InvariantCulture));
e.Add("Instance.PerSeconds", rateLimits.ForInstance.PerSeconds.ToString(CultureInfo.InvariantCulture));
if (rateLimits.ForInstance.AllowBurstForSeconds.HasValue)
{
e.Add("Instance.BurstSeconds", rateLimits.ForInstance.AllowBurstForSeconds.Value.ToString(CultureInfo.InvariantCulture));
}
}
if (rateLimits.ForEnvironment is not null)
{
e.Add("Environment.MaxRequests", rateLimits.ForEnvironment.MaxRequests.ToString(CultureInfo.InvariantCulture));
e.Add("Environment.PerSeconds", rateLimits.ForEnvironment.PerSeconds.ToString(CultureInfo.InvariantCulture));
e.Add("Environment.ValkeyBucket", rateLimits.ForEnvironment.ValkeyBucket);
e.Add("Environment.RouteCount", routeCount.ToString(CultureInfo.InvariantCulture));
if (rateLimits.ForEnvironment.CircuitBreaker is not null)
{
e.Add("CircuitBreaker.Enabled", "true");
e.Add("CircuitBreaker.FailureThreshold", rateLimits.ForEnvironment.CircuitBreaker.FailureThreshold.ToString(CultureInfo.InvariantCulture));
}
}
})
.WithVerification("stella doctor --check check.sources.mirror.ratelimit")
.Build());
}
}

View File

@@ -0,0 +1,186 @@
// -----------------------------------------------------------------------------
// SourceConnectivityCheck.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: Individual source connectivity check with detailed remediation
// -----------------------------------------------------------------------------
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Concelier.Core.Sources;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
namespace StellaOps.Doctor.Plugins.Sources.Checks;
/// <summary>
/// Connectivity check for a single advisory data source.
/// Provides detailed error messages and remediation steps when connectivity fails.
/// </summary>
public sealed class SourceConnectivityCheck : IDoctorCheck
{
private readonly string _sourceId;
private readonly string _displayName;
/// <summary>
/// Creates a new source connectivity check for the specified source.
/// </summary>
/// <param name="sourceId">Source identifier.</param>
/// <param name="displayName">Human-readable source name.</param>
public SourceConnectivityCheck(string sourceId, string displayName)
{
_sourceId = sourceId;
_displayName = displayName;
}
/// <inheritdoc />
public string CheckId => $"check.sources.{_sourceId.ToLowerInvariant()}.connectivity";
/// <inheritdoc />
public string Name => $"{_displayName} Connectivity";
/// <inheritdoc />
public string Description => $"Verifies connectivity to {_displayName} advisory data source";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["connectivity", "sources", _sourceId.ToLowerInvariant()];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(15);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var registry = context.Services.GetService<ISourceRegistry>();
return registry?.GetSource(_sourceId) is not null;
}
/// <inheritdoc />
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Data.ToString());
var registry = context.Services.GetService<ISourceRegistry>();
if (registry is null)
{
return result
.Skip("ISourceRegistry not available")
.Build();
}
var source = registry.GetSource(_sourceId);
if (source is null)
{
return result
.Skip($"Source {_sourceId} not found in registry")
.WithEvidence("Source lookup", e => e.Add("SourceId", _sourceId))
.Build();
}
// Perform connectivity check
var checkResult = await registry.CheckConnectivityAsync(_sourceId, ct);
if (checkResult.IsHealthy)
{
return result
.Pass($"{_displayName} is reachable (latency: {checkResult.Latency?.TotalMilliseconds:F0}ms)")
.WithEvidence("Connectivity check", e =>
{
e.Add("SourceId", _sourceId);
e.Add("DisplayName", _displayName);
e.Add("Status", checkResult.Status.ToString());
e.Add("LatencyMs", checkResult.Latency?.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) ?? "N/A");
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
e.Add("Category", source.Category.ToString());
e.Add("HealthCheckEndpoint", source.HealthCheckEndpoint);
})
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
if (checkResult.Status == SourceConnectivityStatus.Degraded)
{
return result
.Warn($"{_displayName} is degraded: {checkResult.ErrorMessage}")
.WithEvidence("Connectivity check", e =>
{
e.Add("SourceId", _sourceId);
e.Add("DisplayName", _displayName);
e.Add("Status", checkResult.Status.ToString());
e.Add("ErrorCode", checkResult.ErrorCode ?? "UNKNOWN");
e.Add("ErrorMessage", checkResult.ErrorMessage ?? "No details available");
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
if (checkResult.HttpStatusCode.HasValue)
{
e.Add("HttpStatusCode", checkResult.HttpStatusCode.Value.ToString(CultureInfo.InvariantCulture));
}
})
.WithCauses(checkResult.PossibleReasons.ToArray())
.WithRemediation(r => BuildRemediation(r, checkResult))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
// Failed status
return result
.Fail($"{_displayName} connectivity failed: {checkResult.ErrorMessage}")
.WithEvidence("Connectivity check", e =>
{
e.Add("SourceId", _sourceId);
e.Add("DisplayName", _displayName);
e.Add("Status", checkResult.Status.ToString());
e.Add("ErrorCode", checkResult.ErrorCode ?? "UNKNOWN");
e.Add("ErrorMessage", checkResult.ErrorMessage ?? "No details available");
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
e.Add("HealthCheckEndpoint", source.HealthCheckEndpoint);
if (checkResult.HttpStatusCode.HasValue)
{
e.Add("HttpStatusCode", checkResult.HttpStatusCode.Value.ToString(CultureInfo.InvariantCulture));
}
if (checkResult.Latency.HasValue)
{
e.Add("LatencyMs", checkResult.Latency.Value.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture));
}
})
.WithCauses(checkResult.PossibleReasons.ToArray())
.WithRemediation(r => BuildRemediation(r, checkResult))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
private static void BuildRemediation(RemediationBuilder builder, SourceConnectivityResult checkResult)
{
foreach (var step in checkResult.RemediationSteps)
{
if (!string.IsNullOrEmpty(step.Command))
{
var commandType = MapCommandType(step.CommandType);
builder.AddStep(step.Order, step.Description, step.Command, commandType);
}
else
{
builder.AddManualStep(step.Order, step.Description, step.Description);
}
}
}
/// <summary>
/// Maps Concelier CommandType to Doctor CommandType.
/// </summary>
private static Doctor.Models.CommandType MapCommandType(Concelier.Core.Sources.CommandType sourceType)
{
return sourceType switch
{
Concelier.Core.Sources.CommandType.Bash => Doctor.Models.CommandType.Shell,
Concelier.Core.Sources.CommandType.PowerShell => Doctor.Models.CommandType.Shell,
Concelier.Core.Sources.CommandType.StellaCli => Doctor.Models.CommandType.Shell,
Concelier.Core.Sources.CommandType.Url => Doctor.Models.CommandType.Api,
Concelier.Core.Sources.CommandType.EnvVar => Doctor.Models.CommandType.Manual,
_ => Doctor.Models.CommandType.Shell
};
}
}

View File

@@ -0,0 +1,115 @@
// -----------------------------------------------------------------------------
// SourceModeConfiguredCheck.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: Check that source mode is properly configured
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using StellaOps.Concelier.Core.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Sources.Checks;
/// <summary>
/// Verifies that the advisory source mode is properly configured.
/// </summary>
public sealed class SourceModeConfiguredCheck : IDoctorCheck
{
/// <inheritdoc />
public string CheckId => "check.sources.mode.configured";
/// <inheritdoc />
public string Name => "Source Mode Configuration";
/// <inheritdoc />
public string Description => "Verifies that the advisory source mode (upstream/mirror/hybrid) is properly configured";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["configuration", "sources"];
/// <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.sources", DoctorCategory.Data.ToString());
var sourcesConfig = context.Configuration
.GetSection("sources")
.Get<SourcesConfiguration>();
if (sourcesConfig is null)
{
return Task.FromResult(result
.Warn("Sources configuration section not found")
.WithEvidence("Configuration", e =>
{
e.Add("ConfigSection", "sources");
e.Add("Status", "Missing");
})
.WithCauses(
"Configuration file missing 'sources' section",
"Configuration not loaded properly")
.WithRemediation(r => r
.AddManualStep(1, "Add sources section to configuration", "Add 'sources:' section to appsettings.json or environment-specific config")
.AddShellStep(2, "Run setup wizard", "stella setup --step sources"))
.WithVerification("stella doctor --check check.sources.mode.configured")
.Build());
}
// Check mode is valid
var modeStr = sourcesConfig.Mode.ToString();
// Check if at least one source type is configured
var hasUpstream = sourcesConfig.Mode is SourceMode.Direct or SourceMode.Hybrid;
var hasMirror = sourcesConfig.Mode is SourceMode.Mirror or SourceMode.Hybrid;
if (sourcesConfig.Mode == SourceMode.Mirror && sourcesConfig.MirrorServer is null)
{
return Task.FromResult(result
.Warn("Mirror mode configured but mirror server settings missing")
.WithEvidence("Configuration", e =>
{
e.Add("Mode", modeStr);
e.Add("MirrorServerConfigured", "false");
})
.WithCauses(
"Mirror server configuration section missing",
"Mirror server URL not specified")
.WithRemediation(r => r
.AddManualStep(1, "Configure mirror server", "Add 'sources:mirrorServer' section with URL and authentication settings")
.AddShellStep(2, "Run setup wizard", "stella setup --step sources"))
.WithVerification("stella doctor --check check.sources.mode.configured")
.Build());
}
// Count enabled sources
var enabledCount = sourcesConfig.Sources?.Count(s => s.Value.Enabled) ?? 0;
return Task.FromResult(result
.Pass($"Source mode '{modeStr}' configured with {enabledCount} enabled source(s)")
.WithEvidence("Source configuration", e =>
{
e.Add("Mode", modeStr);
e.Add("EnabledSources", enabledCount.ToString());
e.Add("HasUpstreamSources", hasUpstream.ToString());
e.Add("HasMirrorSources", hasMirror.ToString());
e.Add("AutoEnableHealthy", sourcesConfig.AutoEnableHealthySources.ToString());
if (sourcesConfig.MirrorServer is not null)
{
e.Add("MirrorServerEnabled", sourcesConfig.MirrorServer.Enabled.ToString());
}
})
.WithVerification("stella doctor --check check.sources.mode.configured")
.Build());
}
}

View File

@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------------
// SourcesPluginExtensions.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: DI extension for registering Sources Doctor Plugin
// -----------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Doctor.Plugins;
namespace StellaOps.Doctor.Plugins.Sources.DependencyInjection;
/// <summary>
/// Extension methods for registering the Sources Doctor plugin.
/// </summary>
public static class SourcesPluginExtensions
{
/// <summary>
/// Adds the Doctor Sources plugin to the service collection.
/// This plugin provides connectivity checks for advisory data sources.
/// </summary>
/// <param name="services">Service collection.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddDoctorSourcesPlugin(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDoctorPlugin, SourcesPlugin>());
return services;
}
}

View File

@@ -0,0 +1,78 @@
// -----------------------------------------------------------------------------
// SourcesPlugin.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: Doctor plugin providing advisory source connectivity diagnostics
// -----------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Concelier.Core.Sources;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Sources.Checks;
namespace StellaOps.Doctor.Plugins.Sources;
/// <summary>
/// Doctor plugin for advisory data source diagnostics.
/// Provides connectivity checks for all configured CVE/advisory data sources.
/// </summary>
public sealed class SourcesPlugin : IDoctorPlugin
{
/// <inheritdoc />
public string PluginId => "stellaops.doctor.sources";
/// <inheritdoc />
public string DisplayName => "Advisory Sources";
/// <inheritdoc />
public DoctorCategory Category => DoctorCategory.Data;
/// <inheritdoc />
public Version Version => new(1, 0, 0);
/// <inheritdoc />
public Version MinEngineVersion => new(1, 0, 0);
/// <inheritdoc />
public bool IsAvailable(IServiceProvider services)
{
// Plugin is available if ISourceRegistry is registered
return services.GetService<ISourceRegistry>() is not null;
}
/// <inheritdoc />
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context)
{
var registry = context.Services.GetService<ISourceRegistry>();
if (registry is null)
{
return [];
}
var checks = new List<IDoctorCheck>
{
// Overall source mode configuration check
new SourceModeConfiguredCheck(),
// Mirror server checks
new MirrorServerAuthCheck(),
new MirrorServerRateLimitCheck()
};
// Generate dynamic checks for each registered source
foreach (var source in registry.GetAllSources())
{
checks.Add(new SourceConnectivityCheck(source.Id, source.DisplayName));
}
return checks;
}
/// <inheritdoc />
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct)
{
// No initialization required
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RootNamespace>StellaOps.Doctor.Plugins.Sources</RootNamespace>
<Description>Doctor plugin for advisory data source connectivity diagnostics</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>