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
}
}