test fixes and new product advisories work

This commit is contained in:
master
2026-01-28 02:30:48 +02:00
parent 82caceba56
commit 644887997c
288 changed files with 69101 additions and 375 deletions

View File

@@ -0,0 +1,399 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the BUSL-1.1 license.
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Attestor.EvidencePack.Models;
namespace StellaOps.Attestor.EvidencePack.Tests;
/// <summary>
/// Unit tests for ReleaseEvidencePackBuilder.
/// </summary>
public class ReleaseEvidencePackBuilderTests
{
private readonly ILogger<ReleaseEvidencePackBuilder> _logger =
NullLogger<ReleaseEvidencePackBuilder>.Instance;
[Fact]
public void Build_WithAllRequiredFields_ReturnsValidManifest()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
// Act
var manifest = builder.Build();
// Assert
manifest.Should().NotBeNull();
manifest.BundleFormatVersion.Should().Be("1.0.0");
manifest.ReleaseVersion.Should().Be("2.5.0");
manifest.SourceCommit.Should().Be("abc123def456abc123def456abc123def456abc123");
manifest.SourceDateEpoch.Should().Be(1705315800);
manifest.SigningKeyFingerprint.Should().Be("SHA256:abc123...");
manifest.Artifacts.Should().HaveCount(1);
}
[Fact]
public void Build_ComputesManifestHash()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
// Act
var manifest = builder.Build();
// Assert
manifest.ManifestHash.Should().NotBeNullOrWhiteSpace();
manifest.ManifestHash.Should().HaveLength(64); // SHA-256 hex string
manifest.ManifestHash.Should().MatchRegex("^[a-f0-9]{64}$");
}
[Fact]
public void Build_SetsCreatedAtToUtcNowIfNotProvided()
{
// Arrange
var before = DateTimeOffset.UtcNow;
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
// Act
var manifest = builder.Build();
var after = DateTimeOffset.UtcNow;
// Assert
manifest.CreatedAt.Should().BeOnOrAfter(before);
manifest.CreatedAt.Should().BeOnOrBefore(after);
}
[Fact]
public void Build_UsesProvidedCreatedAt()
{
// Arrange
var customTimestamp = new DateTimeOffset(2025, 1, 15, 10, 30, 0, TimeSpan.Zero);
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.WithCreatedAt(customTimestamp)
.AddArtifact(CreateTestArtifact());
// Act
var manifest = builder.Build();
// Assert
manifest.CreatedAt.Should().Be(customTimestamp);
}
[Fact]
public void Build_WithoutReleaseVersion_ThrowsInvalidOperationException()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
// Act & Assert
var act = () => builder.Build();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Release version is required*");
}
[Fact]
public void Build_WithoutSourceCommit_ThrowsInvalidOperationException()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
// Act & Assert
var act = () => builder.Build();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Source commit is required*");
}
[Fact]
public void Build_WithoutSourceDateEpoch_ThrowsInvalidOperationException()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
// Act & Assert
var act = () => builder.Build();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*SOURCE_DATE_EPOCH is required*");
}
[Fact]
public void Build_WithoutSigningKeyFingerprint_ThrowsInvalidOperationException()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.AddArtifact(CreateTestArtifact());
// Act & Assert
var act = () => builder.Build();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*Signing key fingerprint is required*");
}
[Fact]
public void Build_WithoutArtifacts_ThrowsInvalidOperationException()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...");
// Act & Assert
var act = () => builder.Build();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*At least one artifact is required*");
}
[Fact]
public void AddArtifact_AddsToManifest()
{
// Arrange
var builder = CreateValidBuilder();
var artifact = new ArtifactEntry
{
Path = "artifacts/stella-2.5.0-linux-arm64.tar.gz",
Name = "Stella CLI (Linux ARM64)",
Platform = "linux-arm64",
Sha256 = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
Size = 11223344
};
// Act
builder.AddArtifact(artifact);
var manifest = builder.Build();
// Assert
manifest.Artifacts.Should().HaveCount(2);
manifest.Artifacts.Should().Contain(a => a.Platform == "linux-arm64");
}
[Fact]
public void AddArtifact_AddsChecksumEntry()
{
// Arrange
var builder = CreateValidBuilder();
var artifact = new ArtifactEntry
{
Path = "artifacts/stella-2.5.0-linux-arm64.tar.gz",
Name = "Stella CLI (Linux ARM64)",
Platform = "linux-arm64",
Sha256 = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
Sha512 = "b" + new string('c', 127),
Size = 11223344
};
// Act
builder.AddArtifact(artifact);
var manifest = builder.Build();
// Assert
manifest.Checksums.Should().ContainKey("artifacts/stella-2.5.0-linux-arm64.tar.gz");
var checksum = manifest.Checksums["artifacts/stella-2.5.0-linux-arm64.tar.gz"];
checksum.Sha256.Should().Be(artifact.Sha256);
checksum.Sha512.Should().Be(artifact.Sha512);
checksum.Size.Should().Be(artifact.Size);
}
[Fact]
public void AddSbom_AddsToManifest()
{
// Arrange
var builder = CreateValidBuilder();
var sbom = new SbomReference
{
Path = "sbom/stella-cli.cdx.json",
Format = "cyclonedx-json",
SpecVersion = "1.5",
ForArtifact = "stella-2.5.0-linux-x64.tar.gz",
Sha256 = "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
};
// Act
builder.AddSbom(sbom);
var manifest = builder.Build();
// Assert
manifest.Sboms.Should().HaveCount(1);
manifest.Sboms[0].Format.Should().Be("cyclonedx-json");
}
[Fact]
public void AddProvenance_AddsToManifest()
{
// Arrange
var builder = CreateValidBuilder();
var provenance = new ProvenanceReference
{
Path = "provenance/stella-cli.slsa.intoto.jsonl",
PredicateType = "https://slsa.dev/provenance/v1",
ForArtifact = "stella-2.5.0-linux-x64.tar.gz",
BuilderId = "https://ci.stella-ops.org/builder/v1",
SlsaLevel = 2,
Sha256 = "d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5"
};
// Act
builder.AddProvenance(provenance);
var manifest = builder.Build();
// Assert
manifest.ProvenanceStatements.Should().HaveCount(1);
manifest.ProvenanceStatements[0].SlsaLevel.Should().Be(2);
}
[Fact]
public void AddAttestation_AddsToManifest()
{
// Arrange
var builder = CreateValidBuilder();
var attestation = new AttestationReference
{
Path = "attestations/build-attestation.dsse.json",
Type = "dsse",
Description = "Build attestation",
Sha256 = "e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"
};
// Act
builder.AddAttestation(attestation);
var manifest = builder.Build();
// Assert
manifest.Attestations.Should().HaveCount(1);
manifest.Attestations[0].Type.Should().Be("dsse");
}
[Fact]
public void AddRekorProof_AddsToManifest()
{
// Arrange
var builder = CreateValidBuilder();
var proof = new RekorProofEntry
{
Uuid = "abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
LogIndex = 12345678,
IntegratedTime = 1705315800,
ForArtifact = "stella-2.5.0-linux-x64.tar.gz",
InclusionProofPath = "rekor-proofs/log-entries/abc123.json"
};
// Act
builder.AddRekorProof(proof);
var manifest = builder.Build();
// Assert
manifest.RekorProofs.Should().HaveCount(1);
manifest.RekorProofs[0].LogIndex.Should().Be(12345678);
}
[Fact]
public void FluentApi_AllowsChaining()
{
// Arrange & Act
var manifest = new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.WithRekorLogId("rekor-log-id-123")
.WithCreatedAt(DateTimeOffset.UtcNow)
.AddArtifact(CreateTestArtifact())
.Build();
// Assert
manifest.Should().NotBeNull();
manifest.RekorLogId.Should().Be("rekor-log-id-123");
}
[Fact]
public void WithReleaseVersion_ThrowsOnNull()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger);
// Act & Assert
var act = () => builder.WithReleaseVersion(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void WithSourceCommit_ThrowsOnNull()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger);
// Act & Assert
var act = () => builder.WithSourceCommit(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void AddArtifact_ThrowsOnNull()
{
// Arrange
var builder = new ReleaseEvidencePackBuilder(_logger);
// Act & Assert
var act = () => builder.AddArtifact(null!);
act.Should().Throw<ArgumentNullException>();
}
private ReleaseEvidencePackBuilder CreateValidBuilder()
{
return new ReleaseEvidencePackBuilder(_logger)
.WithReleaseVersion("2.5.0")
.WithSourceCommit("abc123def456abc123def456abc123def456abc123")
.WithSourceDateEpoch(1705315800)
.WithSigningKeyFingerprint("SHA256:abc123...")
.AddArtifact(CreateTestArtifact());
}
private static ArtifactEntry CreateTestArtifact()
{
return new ArtifactEntry
{
Path = "artifacts/stella-2.5.0-linux-x64.tar.gz",
Name = "Stella CLI (Linux x64)",
Platform = "linux-x64",
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
Size = 12345678
};
}
}

View File

@@ -0,0 +1,269 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the BUSL-1.1 license.
using System.Collections.Immutable;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Attestor.EvidencePack.Models;
namespace StellaOps.Attestor.EvidencePack.Tests;
/// <summary>
/// Unit tests for ReleaseEvidencePackManifest model serialization.
/// </summary>
public class ReleaseEvidencePackManifestTests
{
[Fact]
public void Manifest_SerializesToJson_WithCorrectPropertyNames()
{
// Arrange
var manifest = CreateValidManifest();
// Act
var json = JsonSerializer.Serialize(manifest);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
root.TryGetProperty("bundleFormatVersion", out _).Should().BeTrue();
root.TryGetProperty("releaseVersion", out _).Should().BeTrue();
root.TryGetProperty("createdAt", out _).Should().BeTrue();
root.TryGetProperty("sourceCommit", out _).Should().BeTrue();
root.TryGetProperty("sourceDateEpoch", out _).Should().BeTrue();
root.TryGetProperty("artifacts", out _).Should().BeTrue();
root.TryGetProperty("checksums", out _).Should().BeTrue();
root.TryGetProperty("sboms", out _).Should().BeTrue();
root.TryGetProperty("provenanceStatements", out _).Should().BeTrue();
root.TryGetProperty("attestations", out _).Should().BeTrue();
root.TryGetProperty("rekorProofs", out _).Should().BeTrue();
root.TryGetProperty("signingKeyFingerprint", out _).Should().BeTrue();
}
[Fact]
public void Manifest_RoundTrips_Successfully()
{
// Arrange
var original = CreateValidManifest();
// Act
var json = JsonSerializer.Serialize(original);
var deserialized = JsonSerializer.Deserialize<ReleaseEvidencePackManifest>(json);
// Assert
deserialized.Should().NotBeNull();
deserialized!.BundleFormatVersion.Should().Be(original.BundleFormatVersion);
deserialized.ReleaseVersion.Should().Be(original.ReleaseVersion);
deserialized.SourceCommit.Should().Be(original.SourceCommit);
deserialized.SourceDateEpoch.Should().Be(original.SourceDateEpoch);
deserialized.Artifacts.Should().HaveCount(original.Artifacts.Length);
deserialized.SigningKeyFingerprint.Should().Be(original.SigningKeyFingerprint);
}
[Fact]
public void ArtifactEntry_SerializesCorrectly()
{
// Arrange
var artifact = new ArtifactEntry
{
Path = "artifacts/stella-2.5.0-linux-x64.tar.gz",
Name = "Stella CLI",
Platform = "linux-x64",
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
Sha512 = "a" + new string('b', 127),
Size = 12345678,
SignaturePath = "artifacts/stella-2.5.0-linux-x64.tar.gz.sig"
};
// Act
var json = JsonSerializer.Serialize(artifact);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
root.GetProperty("path").GetString().Should().Be(artifact.Path);
root.GetProperty("name").GetString().Should().Be(artifact.Name);
root.GetProperty("platform").GetString().Should().Be(artifact.Platform);
root.GetProperty("sha256").GetString().Should().Be(artifact.Sha256);
root.GetProperty("size").GetInt64().Should().Be(artifact.Size);
root.GetProperty("signaturePath").GetString().Should().Be(artifact.SignaturePath);
}
[Fact]
public void ChecksumEntry_SerializesCorrectly()
{
// Arrange
var checksum = new ChecksumEntry
{
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
Sha512 = "a" + new string('b', 127),
Size = 12345678
};
// Act
var json = JsonSerializer.Serialize(checksum);
var deserialized = JsonSerializer.Deserialize<ChecksumEntry>(json);
// Assert
deserialized.Should().NotBeNull();
deserialized!.Sha256.Should().Be(checksum.Sha256);
deserialized.Sha512.Should().Be(checksum.Sha512);
deserialized.Size.Should().Be(checksum.Size);
}
[Fact]
public void SbomReference_SerializesCorrectly()
{
// Arrange
var sbom = new SbomReference
{
Path = "sbom/stella-cli.cdx.json",
Format = "cyclonedx-json",
SpecVersion = "1.5",
ForArtifact = "stella-2.5.0-linux-x64.tar.gz",
SignaturePath = "sbom/stella-cli.cdx.json.sig",
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
};
// Act
var json = JsonSerializer.Serialize(sbom);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
root.GetProperty("path").GetString().Should().Be(sbom.Path);
root.GetProperty("format").GetString().Should().Be(sbom.Format);
root.GetProperty("specVersion").GetString().Should().Be(sbom.SpecVersion);
root.GetProperty("forArtifact").GetString().Should().Be(sbom.ForArtifact);
}
[Fact]
public void ProvenanceReference_SerializesCorrectly()
{
// Arrange
var provenance = new ProvenanceReference
{
Path = "provenance/stella-cli.slsa.intoto.jsonl",
PredicateType = "https://slsa.dev/provenance/v1",
ForArtifact = "stella-2.5.0-linux-x64.tar.gz",
SignaturePath = "provenance/stella-cli.slsa.intoto.jsonl.sig",
BuilderId = "https://ci.stella-ops.org/builder/v1",
SlsaLevel = 2,
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
};
// Act
var json = JsonSerializer.Serialize(provenance);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
root.GetProperty("predicateType").GetString().Should().Be(provenance.PredicateType);
root.GetProperty("builderId").GetString().Should().Be(provenance.BuilderId);
root.GetProperty("slsaLevel").GetInt32().Should().Be(2);
}
[Fact]
public void RekorProofEntry_SerializesCorrectly()
{
// Arrange
var proof = new RekorProofEntry
{
Uuid = "abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
LogIndex = 12345678,
IntegratedTime = 1705315800,
ForArtifact = "stella-2.5.0-linux-x64.tar.gz",
InclusionProofPath = "rekor-proofs/log-entries/abc123.json"
};
// Act
var json = JsonSerializer.Serialize(proof);
var deserialized = JsonSerializer.Deserialize<RekorProofEntry>(json);
// Assert
deserialized.Should().NotBeNull();
deserialized!.Uuid.Should().Be(proof.Uuid);
deserialized.LogIndex.Should().Be(proof.LogIndex);
deserialized.IntegratedTime.Should().Be(proof.IntegratedTime);
deserialized.ForArtifact.Should().Be(proof.ForArtifact);
}
[Fact]
public void Manifest_OptionalFieldsOmittedWhenNull()
{
// Arrange
var manifest = CreateValidManifest();
// Act
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
var json = JsonSerializer.Serialize(manifest, options);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - RekorLogId is null in the test manifest
root.TryGetProperty("rekorLogId", out _).Should().BeFalse();
}
[Fact]
public void Manifest_ArtifactsArrayIsImmutable()
{
// Arrange
var manifest = CreateValidManifest();
// Assert - ImmutableArray cannot be modified
manifest.Artifacts.Should().BeOfType<ImmutableArray<ArtifactEntry>>();
}
[Fact]
public void Manifest_ChecksumsDictionaryIsImmutable()
{
// Arrange
var manifest = CreateValidManifest();
// Assert - ImmutableDictionary cannot be modified
manifest.Checksums.Should().BeAssignableTo<IImmutableDictionary<string, ChecksumEntry>>();
}
private static ReleaseEvidencePackManifest CreateValidManifest()
{
var artifacts = ImmutableArray.Create(
new ArtifactEntry
{
Path = "artifacts/stella-2.5.0-linux-x64.tar.gz",
Name = "Stella CLI (Linux x64)",
Platform = "linux-x64",
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
Size = 12345678
}
);
var checksums = ImmutableDictionary.CreateRange(new[]
{
KeyValuePair.Create(
"artifacts/stella-2.5.0-linux-x64.tar.gz",
new ChecksumEntry
{
Sha256 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
Size = 12345678
})
});
return new ReleaseEvidencePackManifest
{
BundleFormatVersion = "1.0.0",
ReleaseVersion = "2.5.0",
CreatedAt = new DateTimeOffset(2025, 1, 15, 10, 30, 0, TimeSpan.Zero),
SourceCommit = "abc123def456abc123def456abc123def456abc123",
SourceDateEpoch = 1705315800,
Artifacts = artifacts,
Checksums = checksums,
Sboms = ImmutableArray<SbomReference>.Empty,
ProvenanceStatements = ImmutableArray<ProvenanceReference>.Empty,
Attestations = ImmutableArray<AttestationReference>.Empty,
RekorProofs = ImmutableArray<RekorProofEntry>.Empty,
SigningKeyFingerprint = "SHA256:abc123def456..."
};
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.EvidencePack\StellaOps.Attestor.EvidencePack.csproj" />
</ItemGroup>
</Project>