audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user