Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Verification/Checks/SbomValidationCheck.cs
master c58a236d70 Doctor plugin checks: implement health check classes and documentation
Implement remediation-aware health checks across all Doctor plugin modules
(Agent, Attestor, Auth, BinaryAnalysis, Compliance, Crypto, Environment,
EvidenceLocker, Notify, Observability, Operations, Policy, Postgres, Release,
Scanner, Storage, Vex) and their backing library counterparts (AI, Attestation,
Authority, Core, Cryptography, Database, Docker, Integration, Notify,
Observability, Security, ServiceGraph, Sources, Verification).

Each check now emits structured remediation metadata (severity, category,
runbook links, and fix suggestions) consumed by the Doctor dashboard
remediation panel.

Also adds:
- docs/doctor/articles/ knowledge base for check explanations
- Advisory AI search seed and allowlist updates for doctor content
- Sprint plan for doctor checks documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:28:00 +02:00

229 lines
8.9 KiB
C#

using Microsoft.Extensions.Configuration;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
using StellaOps.Doctor.Plugins.Verification.Configuration;
using System.Text.Json;
namespace StellaOps.Doctor.Plugins.Verification.Checks;
/// <summary>
/// Verifies SBOM validation for test artifact.
/// </summary>
public sealed class SbomValidationCheck : VerificationCheckBase
{
/// <inheritdoc />
public override string CheckId => "check.verification.sbom.validation";
/// <inheritdoc />
public override string Name => "SBOM Validation";
/// <inheritdoc />
public override string Description => "Fetches and validates SBOM for test artifact (CycloneDX/SPDX)";
/// <inheritdoc />
public override IReadOnlyList<string> Tags => ["verification", "sbom", "cyclonedx", "spdx", "supply-chain"];
/// <inheritdoc />
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
/// <inheritdoc />
public override bool CanRun(DoctorPluginContext context)
{
if (!base.CanRun(context))
return false;
var options = VerificationPlugin.GetOptions(context);
return HasTestArtifactConfigured(options);
}
/// <inheritdoc />
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
DoctorPluginContext context,
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
if (!HasTestArtifactConfigured(options))
{
return GetNoTestArtifactConfiguredResult(result, CheckId);
}
// Check offline bundle for SBOM
if (!string.IsNullOrEmpty(options.TestArtifact.OfflineBundlePath))
{
return await ValidateFromOfflineBundle(options, result, ct);
}
// Online SBOM validation
return await ValidateFromOnline(context, options, result, ct);
}
private static Task<DoctorCheckResult> ValidateFromOfflineBundle(
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
var bundlePath = options.TestArtifact.OfflineBundlePath!;
if (!File.Exists(bundlePath))
{
return Task.FromResult(result
.Fail($"Offline bundle not found: {bundlePath}")
.WithEvidence("SBOM validation", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("FileExists", "false"))
.WithRemediation(r => r
.AddShellStep(1, "Export bundle", "stella verification bundle export --include-sbom --output " + bundlePath)
.WithRunbookUrl(""))
.WithVerification($"stella doctor --check check.verification.sbom.validation")
.Build());
}
try
{
var content = File.ReadAllText(bundlePath);
// Detect SBOM format
var (format, version, componentCount) = DetectSbomFormat(content);
if (format == SbomFormat.None)
{
return Task.FromResult(result
.Fail("No valid SBOM found in offline bundle")
.WithEvidence("SBOM validation", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("SbomFound", "false"))
.WithCauses(
"Bundle was exported without SBOM",
"Test artifact has no SBOM attached")
.WithRemediation(r => r
.AddShellStep(1, "Re-export with SBOM", "stella verification bundle export --include-sbom --output " + bundlePath)
.AddManualStep(2, "Generate SBOM", "Enable SBOM generation in your build pipeline")
.WithRunbookUrl(""))
.WithVerification($"stella doctor --check check.verification.sbom.validation")
.Build());
}
return Task.FromResult(result
.Pass($"SBOM valid ({format} {version}, {componentCount} components)")
.WithEvidence("SBOM validation", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("Format", format.ToString())
.Add("Version", version ?? "(unknown)")
.Add("ComponentCount", componentCount.ToString()))
.Build());
}
catch (Exception ex)
{
return Task.FromResult(result
.Fail($"Cannot read offline bundle: {ex.Message}")
.WithEvidence("SBOM validation", e => e
.Add("Mode", "Offline")
.Add("BundlePath", bundlePath)
.Add("Error", ex.Message))
.Build());
}
}
private static Task<DoctorCheckResult> ValidateFromOnline(
DoctorPluginContext context,
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
var reference = options.TestArtifact.Reference!;
// Note: Full SBOM validation requires the Scanner/Concelier service
// For doctor check, we verify configuration is in place
var sbomGenerationEnabled = context.Configuration.GetValue<bool>("Scanner:SbomGeneration:Enabled");
var sbomAttestationEnabled = context.Configuration.GetValue<bool>("Attestor:SbomAttestation:Enabled");
if (!sbomGenerationEnabled && !sbomAttestationEnabled)
{
return Task.FromResult(result
.Warn("SBOM generation and attestation not enabled")
.WithEvidence("SBOM validation", e => e
.Add("Mode", "Online")
.Add("Reference", reference)
.Add("SbomGenerationEnabled", sbomGenerationEnabled.ToString())
.Add("SbomAttestationEnabled", sbomAttestationEnabled.ToString())
.Add("Note", "Enable SBOM generation to attach SBOMs to artifacts"))
.WithCauses(
"SBOM generation not configured",
"SBOM attestation not configured")
.WithRemediation(r => r
.AddManualStep(1, "Enable SBOM generation", "Set Scanner:SbomGeneration:Enabled to true")
.AddManualStep(2, "Enable SBOM attestation", "Set Attestor:SbomAttestation:Enabled to true")
.WithRunbookUrl(""))
.WithVerification($"stella doctor --check check.verification.sbom.validation")
.Build());
}
return Task.FromResult(result
.Pass("SBOM generation/attestation configured")
.WithEvidence("SBOM validation", e => e
.Add("Mode", "Online")
.Add("Reference", reference)
.Add("SbomGenerationEnabled", sbomGenerationEnabled.ToString())
.Add("SbomAttestationEnabled", sbomAttestationEnabled.ToString())
.Add("Note", "Full SBOM validation requires runtime scanner service"))
.Build());
}
private static (SbomFormat Format, string? Version, int ComponentCount) DetectSbomFormat(string content)
{
try
{
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
// Check for CycloneDX
if (root.TryGetProperty("bomFormat", out var bomFormat) &&
bomFormat.GetString()?.Equals("CycloneDX", StringComparison.OrdinalIgnoreCase) == true)
{
var version = root.TryGetProperty("specVersion", out var sv) ? sv.GetString() : null;
var componentCount = root.TryGetProperty("components", out var c) && c.ValueKind == JsonValueKind.Array
? c.GetArrayLength()
: 0;
return (SbomFormat.CycloneDX, version, componentCount);
}
// Check for SPDX
if (root.TryGetProperty("spdxVersion", out var spdxVersion))
{
var version = spdxVersion.GetString();
var componentCount = root.TryGetProperty("packages", out var p) && p.ValueKind == JsonValueKind.Array
? p.GetArrayLength()
: 0;
return (SbomFormat.SPDX, version, componentCount);
}
// Check for embedded SBOM in bundle
if (root.TryGetProperty("sbom", out var sbomElement))
{
var sbomContent = sbomElement.GetRawText();
return DetectSbomFormat(sbomContent);
}
}
catch
{
// Not valid JSON or parsing failed
}
return (SbomFormat.None, null, 0);
}
private enum SbomFormat
{
None,
CycloneDX,
SPDX
}
}