new two advisories and sprints work on them
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Builders;
|
||||
using StellaOps.Doctor.Plugins.Verification.Configuration;
|
||||
|
||||
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))
|
||||
.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"))
|
||||
.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"))
|
||||
.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user