130 lines
4.5 KiB
C#
130 lines
4.5 KiB
C#
namespace StellaOps.Interop.Tests.CycloneDx;
|
|
|
|
[Trait("Category", "Interop")]
|
|
[Trait("Format", "CycloneDX")]
|
|
public class CycloneDxRoundTripTests : IClassFixture<InteropTestHarness>
|
|
{
|
|
private readonly InteropTestHarness _harness;
|
|
|
|
public CycloneDxRoundTripTests(InteropTestHarness harness)
|
|
{
|
|
_harness = harness;
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TestImages))]
|
|
public async Task Syft_GeneratesCycloneDx_GrypeCanConsume(string imageRef)
|
|
{
|
|
// Generate SBOM with Syft
|
|
var sbomResult = await _harness.GenerateSbomWithSyft(
|
|
imageRef, SbomFormat.CycloneDx16);
|
|
sbomResult.Success.Should().BeTrue("Syft should generate CycloneDX SBOM");
|
|
|
|
// Scan from SBOM with Grype
|
|
var grypeResult = await _harness.ScanWithGrypeFromSbom(sbomResult.Path!);
|
|
grypeResult.Success.Should().BeTrue("Grype should consume Syft-generated CycloneDX SBOM");
|
|
|
|
// Grype should be able to parse and find vulnerabilities
|
|
grypeResult.Findings.Should().NotBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TestImages))]
|
|
public async Task Stella_GeneratesCycloneDx_GrypeCanConsume(string imageRef)
|
|
{
|
|
// Generate SBOM with Stella
|
|
var sbomResult = await _harness.GenerateSbomWithStella(
|
|
imageRef, SbomFormat.CycloneDx16);
|
|
sbomResult.Success.Should().BeTrue("Stella should generate CycloneDX SBOM");
|
|
|
|
// Scan from SBOM with Grype
|
|
var grypeResult = await _harness.ScanWithGrypeFromSbom(sbomResult.Path!);
|
|
grypeResult.Success.Should().BeTrue("Grype should consume Stella-generated CycloneDX SBOM");
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TestImages))]
|
|
[Trait("Category", "Parity")]
|
|
public async Task Stella_And_Grype_FindingsParity_Above95Percent(string imageRef)
|
|
{
|
|
// Generate SBOM with Stella
|
|
var stellaSbom = await _harness.GenerateSbomWithStella(
|
|
imageRef, SbomFormat.CycloneDx16);
|
|
stellaSbom.Success.Should().BeTrue();
|
|
|
|
// TODO: Get Stella findings from scan result
|
|
var stellaFindings = new List<Finding>();
|
|
|
|
// Scan SBOM with Grype
|
|
var grypeResult = await _harness.ScanWithGrypeFromSbom(stellaSbom.Path!);
|
|
grypeResult.Success.Should().BeTrue();
|
|
|
|
// Compare findings
|
|
var comparison = _harness.CompareFindings(
|
|
stellaFindings,
|
|
grypeResult.Findings!,
|
|
tolerancePercent: 5);
|
|
|
|
comparison.ParityPercent.Should().BeGreaterOrEqualTo(95,
|
|
$"Findings parity {comparison.ParityPercent:F2}% is below 95% threshold. " +
|
|
$"Only in Stella: {comparison.OnlyInStella}, Only in Grype: {comparison.OnlyInGrype}");
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TestImages))]
|
|
[Trait("Category", "Attestation")]
|
|
public async Task CycloneDx_Attestation_RoundTrip(string imageRef)
|
|
{
|
|
// Skip if not in CI - cosign requires credentials
|
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
|
|
{
|
|
// Skip in local dev
|
|
return;
|
|
}
|
|
|
|
// Generate SBOM
|
|
var sbomResult = await _harness.GenerateSbomWithStella(
|
|
imageRef, SbomFormat.CycloneDx16);
|
|
sbomResult.Success.Should().BeTrue();
|
|
|
|
// Attest with cosign
|
|
var attestResult = await _harness.AttestWithCosign(
|
|
sbomResult.Path!, imageRef);
|
|
attestResult.Success.Should().BeTrue("Cosign should attest SBOM");
|
|
|
|
// TODO: Verify attestation
|
|
// var verifyResult = await _harness.VerifyCosignAttestation(imageRef);
|
|
// verifyResult.Success.Should().BeTrue();
|
|
|
|
// Digest should match
|
|
// var attestedDigest = verifyResult.PredicateDigest;
|
|
// attestedDigest.Should().Be(sbomResult.Digest);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Category", "Schema")]
|
|
public async Task Stella_CycloneDx_ValidatesAgainstSchema()
|
|
{
|
|
var imageRef = "alpine:3.18";
|
|
|
|
// Generate SBOM
|
|
var sbomResult = await _harness.GenerateSbomWithStella(
|
|
imageRef, SbomFormat.CycloneDx16);
|
|
sbomResult.Success.Should().BeTrue();
|
|
|
|
// TODO: Validate against CycloneDX 1.6 JSON schema
|
|
sbomResult.Content.Should().NotBeNullOrEmpty();
|
|
sbomResult.Content.Should().Contain("\"bomFormat\": \"CycloneDX\"");
|
|
sbomResult.Content.Should().Contain("\"specVersion\": \"1.6\"");
|
|
}
|
|
|
|
public static IEnumerable<object[]> TestImages =>
|
|
[
|
|
["alpine:3.18"],
|
|
["debian:12-slim"],
|
|
["node:20-alpine"],
|
|
["python:3.12-slim"],
|
|
["golang:1.22-alpine"]
|
|
];
|
|
}
|