Resolve Concelier/Excititor merge conflicts
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
|
||||
|
||||
public sealed class DescriptorGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateAsync_BuildsDeterministicDescriptor()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var document = await generator.CreateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DescriptorGenerator.Schema, document.Schema);
|
||||
Assert.Equal(fakeTime.GetUtcNow(), document.GeneratedAt);
|
||||
Assert.Equal(request.ImageDigest, document.Subject.Digest);
|
||||
Assert.Equal(request.SbomMediaType, document.Artifact.MediaType);
|
||||
Assert.Equal(request.SbomName, document.Artifact.Annotations["org.opencontainers.image.title"]);
|
||||
Assert.Equal("pending", document.Provenance.Status);
|
||||
Assert.Equal(request.AttestorUri, document.Provenance.AttestorUri);
|
||||
Assert.Equal(request.PredicateType, document.Provenance.PredicateType);
|
||||
|
||||
var expectedSbomDigest = ComputeSha256File(sbomPath);
|
||||
Assert.Equal(expectedSbomDigest, document.Artifact.Digest);
|
||||
Assert.Equal(expectedSbomDigest, document.Metadata["sbomDigest"]);
|
||||
|
||||
var expectedDsse = ComputeExpectedDsse(request.ImageDigest, expectedSbomDigest, document.Provenance.Nonce);
|
||||
Assert.Equal(expectedDsse, document.Provenance.ExpectedDsseSha256);
|
||||
Assert.Equal(expectedDsse, document.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"]);
|
||||
Assert.Equal(document.Provenance.Nonce, document.Artifact.Annotations["org.stellaops.provenance.nonce"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_RepeatedInvocationsReuseDeterministicNonce()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var first = await generator.CreateAsync(request, CancellationToken.None);
|
||||
var second = await generator.CreateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(first.Provenance.Nonce, second.Provenance.Nonce);
|
||||
Assert.Equal(first.Provenance.ExpectedDsseSha256, second.Provenance.ExpectedDsseSha256);
|
||||
Assert.Equal(first.Artifact.Annotations["org.stellaops.provenance.nonce"], second.Artifact.Annotations["org.stellaops.provenance.nonce"]);
|
||||
Assert.Equal(first.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"], second.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_MetadataDifferencesYieldDistinctNonce()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var baseline = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main"
|
||||
}.Validate();
|
||||
|
||||
var variant = baseline with
|
||||
{
|
||||
BuildRef = "refs/heads/feature",
|
||||
Repository = "git.stella-ops.org/stellaops/feature"
|
||||
};
|
||||
variant = variant.Validate();
|
||||
|
||||
var baselineDocument = await generator.CreateAsync(baseline, CancellationToken.None);
|
||||
var variantDocument = await generator.CreateAsync(variant, CancellationToken.None);
|
||||
|
||||
Assert.NotEqual(baselineDocument.Provenance.Nonce, variantDocument.Provenance.Nonce);
|
||||
Assert.NotEqual(baselineDocument.Provenance.ExpectedDsseSha256, variantDocument.Provenance.ExpectedDsseSha256);
|
||||
}
|
||||
|
||||
private static string ComputeSha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = SHA256.HashData(stream);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLower(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
|
||||
private static string ComputeExpectedDsse(string imageDigest, string sbomDigest, string nonce)
|
||||
{
|
||||
var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(payload), hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLower(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
|
||||
|
||||
public sealed class DescriptorGoldenTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task DescriptorMatchesBaselineFixture()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
var document = await generator.CreateAsync(request, CancellationToken.None);
|
||||
var actualJson = JsonSerializer.Serialize(document, SerializerOptions);
|
||||
var normalizedJson = NormalizeDescriptorJson(actualJson, Path.GetFileName(sbomPath));
|
||||
|
||||
var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||
var fixturePath = Path.Combine(projectRoot, "Fixtures", "descriptor.baseline.json");
|
||||
var updateRequested = string.Equals(Environment.GetEnvironmentVariable("UPDATE_BUILDX_FIXTURES"), "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (updateRequested)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fixturePath)!);
|
||||
await File.WriteAllTextAsync(fixturePath, normalizedJson);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(fixturePath))
|
||||
{
|
||||
throw new InvalidOperationException($"Baseline fixture '{fixturePath}' is missing. Set UPDATE_BUILDX_FIXTURES=1 and re-run the tests to generate it.");
|
||||
}
|
||||
|
||||
var baselineJson = await File.ReadAllTextAsync(fixturePath);
|
||||
|
||||
using var baselineDoc = JsonDocument.Parse(baselineJson);
|
||||
using var actualDoc = JsonDocument.Parse(normalizedJson);
|
||||
|
||||
AssertJsonEquivalent(baselineDoc.RootElement, actualDoc.RootElement);
|
||||
}
|
||||
|
||||
private static string NormalizeDescriptorJson(string json, string sbomFileName)
|
||||
{
|
||||
var node = JsonNode.Parse(json)?.AsObject()
|
||||
?? throw new InvalidOperationException("Failed to parse descriptor JSON for normalization.");
|
||||
|
||||
if (node["metadata"] is JsonObject metadata)
|
||||
{
|
||||
metadata["sbomPath"] = sbomFileName;
|
||||
}
|
||||
|
||||
return node.ToJsonString(SerializerOptions);
|
||||
}
|
||||
|
||||
private static void AssertJsonEquivalent(JsonElement expected, JsonElement actual)
|
||||
{
|
||||
if (expected.ValueKind != actual.ValueKind)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException($"Value kind mismatch. Expected '{expected.ValueKind}' but found '{actual.ValueKind}'.");
|
||||
}
|
||||
|
||||
switch (expected.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
var expectedProperties = expected.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal);
|
||||
var actualProperties = actual.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal(
|
||||
expectedProperties.Keys.OrderBy(static name => name).ToArray(),
|
||||
actualProperties.Keys.OrderBy(static name => name).ToArray());
|
||||
|
||||
foreach (var propertyName in expectedProperties.Keys)
|
||||
{
|
||||
AssertJsonEquivalent(expectedProperties[propertyName], actualProperties[propertyName]);
|
||||
}
|
||||
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
var expectedItems = expected.EnumerateArray().ToArray();
|
||||
var actualItems = actual.EnumerateArray().ToArray();
|
||||
|
||||
Assert.Equal(expectedItems.Length, actualItems.Length);
|
||||
for (var i = 0; i < expectedItems.Length; i++)
|
||||
{
|
||||
AssertJsonEquivalent(expectedItems[i], actualItems[i]);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
Assert.Equal(expected.ToString(), actual.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user