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; /// /// Verifies SBOM validation for test artifact. /// public sealed class SbomValidationCheck : VerificationCheckBase { /// public override string CheckId => "check.verification.sbom.validation"; /// public override string Name => "SBOM Validation"; /// public override string Description => "Fetches and validates SBOM for test artifact (CycloneDX/SPDX)"; /// public override IReadOnlyList Tags => ["verification", "sbom", "cyclonedx", "spdx", "supply-chain"]; /// public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10); /// public override bool CanRun(DoctorPluginContext context) { if (!base.CanRun(context)) return false; var options = VerificationPlugin.GetOptions(context); return HasTestArtifactConfigured(options); } /// protected override async Task 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 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 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("Scanner:SbomGeneration:Enabled"); var sbomAttestationEnabled = context.Configuration.GetValue("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 } }