tests fixes and some product advisories tunes ups
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
@@ -9,15 +10,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<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" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under the BUSL-1.1 license.
|
||||
// Advisory: EU CRA/NIS2 compliance - Sealed Audit-Pack replay_log.json tests
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Attestor.EvidencePack.Models;
|
||||
using StellaOps.Attestor.EvidencePack.Services;
|
||||
|
||||
namespace StellaOps.Attestor.EvidencePack.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for VerificationReplayLogBuilder.
|
||||
/// Tests the replay_log.json generation for EU CRA/NIS2 compliance.
|
||||
/// </summary>
|
||||
public class VerificationReplayLogBuilderTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly VerificationReplayLogBuilder _builder;
|
||||
|
||||
public VerificationReplayLogBuilderTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 29, 12, 0, 0, TimeSpan.Zero));
|
||||
_builder = new VerificationReplayLogBuilder(_timeProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMinimalRequest_ReturnsValidReplayLog()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerificationReplayLogRequest
|
||||
{
|
||||
ArtifactRef = "oci://registry.example.com/app:v1.0.0@sha256:abc123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Assert
|
||||
log.Should().NotBeNull();
|
||||
log.SchemaVersion.Should().Be("1.0.0");
|
||||
log.ArtifactRef.Should().Be("oci://registry.example.com/app:v1.0.0@sha256:abc123");
|
||||
log.VerifierVersion.Should().Be("stellaops-attestor/1.0.0");
|
||||
log.Result.Should().Be("pass");
|
||||
log.ReplayId.Should().StartWith("replay_");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithFullVerificationRequest_ReturnsAllSteps()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerificationReplayLogRequest
|
||||
{
|
||||
ArtifactRef = "oci://registry.example.com/app:v1.0.0@sha256:abc123",
|
||||
SbomPath = "sbom/app.cdx.json",
|
||||
CanonicalSbomDigest = "sha256:sbomdigest123",
|
||||
DsseEnvelopePath = "attestations/app.dsse.json",
|
||||
DsseSubjectDigest = "sha256:sbomdigest123",
|
||||
DsseSignatureValid = true,
|
||||
SigningKeyId = "cosign-key-1",
|
||||
SignatureAlgorithm = "ecdsa-p256",
|
||||
SigningKeyFingerprint = "SHA256:keyfingerprint123",
|
||||
CosignPublicKeyPath = "cosign.pub",
|
||||
RekorLogId = "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
|
||||
RekorLogIndex = 12345678,
|
||||
RekorTreeSize = 99999999,
|
||||
RekorRootHash = "sha256:merklerootabc",
|
||||
RekorIntegratedTime = 1706529600,
|
||||
InclusionProofPath = "rekor-proofs/log-entries/12345678.json",
|
||||
RekorInclusionValid = true,
|
||||
CheckpointPath = "rekor-proofs/checkpoint.json",
|
||||
CheckpointValid = true,
|
||||
RekorPublicKeyPath = "rekor-public-key.pub",
|
||||
RekorPublicKeyId = "rekor-key-1"
|
||||
};
|
||||
|
||||
// Act
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Assert
|
||||
log.Result.Should().Be("pass");
|
||||
log.Steps.Should().HaveCount(5);
|
||||
|
||||
// Step 1: Canonical SBOM digest
|
||||
log.Steps[0].Step.Should().Be(1);
|
||||
log.Steps[0].Action.Should().Be("compute_canonical_sbom_digest");
|
||||
log.Steps[0].Input.Should().Be("sbom/app.cdx.json");
|
||||
log.Steps[0].Output.Should().Be("sha256:sbomdigest123");
|
||||
log.Steps[0].Result.Should().Be("pass");
|
||||
|
||||
// Step 2: DSSE subject match
|
||||
log.Steps[1].Step.Should().Be(2);
|
||||
log.Steps[1].Action.Should().Be("verify_dsse_subject_match");
|
||||
log.Steps[1].Expected.Should().Be("sha256:sbomdigest123");
|
||||
log.Steps[1].Actual.Should().Be("sha256:sbomdigest123");
|
||||
log.Steps[1].Result.Should().Be("pass");
|
||||
|
||||
// Step 3: DSSE signature
|
||||
log.Steps[2].Step.Should().Be(3);
|
||||
log.Steps[2].Action.Should().Be("verify_dsse_signature");
|
||||
log.Steps[2].KeyId.Should().Be("cosign-key-1");
|
||||
log.Steps[2].Result.Should().Be("pass");
|
||||
|
||||
// Step 4: Rekor inclusion
|
||||
log.Steps[3].Step.Should().Be(4);
|
||||
log.Steps[3].Action.Should().Be("verify_rekor_inclusion");
|
||||
log.Steps[3].Result.Should().Be("pass");
|
||||
|
||||
// Step 5: Rekor checkpoint
|
||||
log.Steps[4].Step.Should().Be(5);
|
||||
log.Steps[4].Action.Should().Be("verify_rekor_checkpoint");
|
||||
log.Steps[4].Result.Should().Be("pass");
|
||||
|
||||
// Verification keys
|
||||
log.VerificationKeys.Should().HaveCount(2);
|
||||
log.VerificationKeys[0].Type.Should().Be("cosign");
|
||||
log.VerificationKeys[0].Path.Should().Be("cosign.pub");
|
||||
log.VerificationKeys[1].Type.Should().Be("rekor");
|
||||
log.VerificationKeys[1].Path.Should().Be("rekor-public-key.pub");
|
||||
|
||||
// Rekor info
|
||||
log.Rekor.Should().NotBeNull();
|
||||
log.Rekor!.LogId.Should().Be("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d");
|
||||
log.Rekor.LogIndex.Should().Be(12345678);
|
||||
log.Rekor.TreeSize.Should().Be(99999999);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithFailedDsseSignature_ReturnsFailResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerificationReplayLogRequest
|
||||
{
|
||||
ArtifactRef = "oci://registry.example.com/app:v1.0.0@sha256:abc123",
|
||||
SbomPath = "sbom/app.cdx.json",
|
||||
CanonicalSbomDigest = "sha256:sbomdigest123",
|
||||
DsseEnvelopePath = "attestations/app.dsse.json",
|
||||
DsseSubjectDigest = "sha256:sbomdigest123",
|
||||
DsseSignatureValid = false,
|
||||
DsseSignatureError = "Invalid signature: key mismatch",
|
||||
SigningKeyId = "cosign-key-1",
|
||||
CosignPublicKeyPath = "cosign.pub"
|
||||
};
|
||||
|
||||
// Act
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Assert
|
||||
log.Result.Should().Be("fail");
|
||||
log.Steps.Should().Contain(s => s.Action == "verify_dsse_signature" && s.Result == "fail");
|
||||
log.Steps.First(s => s.Action == "verify_dsse_signature").Error
|
||||
.Should().Be("Invalid signature: key mismatch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMismatchedDigests_ReturnsFailResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerificationReplayLogRequest
|
||||
{
|
||||
ArtifactRef = "oci://registry.example.com/app:v1.0.0@sha256:abc123",
|
||||
SbomPath = "sbom/app.cdx.json",
|
||||
CanonicalSbomDigest = "sha256:computeddigest",
|
||||
DsseSubjectDigest = "sha256:differentdigest"
|
||||
};
|
||||
|
||||
// Act
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Assert
|
||||
log.Result.Should().Be("fail");
|
||||
var mismatchStep = log.Steps.First(s => s.Action == "verify_dsse_subject_match");
|
||||
mismatchStep.Result.Should().Be("fail");
|
||||
mismatchStep.Expected.Should().Be("sha256:differentdigest");
|
||||
mismatchStep.Actual.Should().Be("sha256:computeddigest");
|
||||
mismatchStep.Error.Should().Contain("does not match");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_ReturnsValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerificationReplayLogRequest
|
||||
{
|
||||
ArtifactRef = "oci://registry.example.com/app:v1.0.0@sha256:abc123",
|
||||
SbomPath = "sbom/app.cdx.json",
|
||||
CanonicalSbomDigest = "sha256:sbomdigest123"
|
||||
};
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Act
|
||||
var json = _builder.Serialize(log);
|
||||
|
||||
// Assert
|
||||
json.Should().NotBeNullOrEmpty();
|
||||
json.Should().Contain("\"schema_version\"");
|
||||
json.Should().Contain("\"replay_id\"");
|
||||
json.Should().Contain("\"artifact_ref\"");
|
||||
json.Should().Contain("\"steps\"");
|
||||
json.Should().Contain("\"compute_canonical_sbom_digest\"");
|
||||
|
||||
// Should be valid JSON
|
||||
var parsed = JsonDocument.Parse(json);
|
||||
parsed.RootElement.GetProperty("schema_version").GetString().Should().Be("1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMetadata_IncludesMetadataInLog()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerificationReplayLogRequest
|
||||
{
|
||||
ArtifactRef = "oci://registry.example.com/app:v1.0.0@sha256:abc123",
|
||||
Metadata = ImmutableDictionary<string, string>.Empty
|
||||
.Add("compliance_framework", "EU_CRA_NIS2")
|
||||
.Add("auditor", "external-auditor-id")
|
||||
};
|
||||
|
||||
// Act
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Assert
|
||||
log.Metadata.Should().NotBeNull();
|
||||
log.Metadata!["compliance_framework"].Should().Be("EU_CRA_NIS2");
|
||||
log.Metadata["auditor"].Should().Be("external-auditor-id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_GeneratesUniqueReplayId()
|
||||
{
|
||||
// Arrange
|
||||
var request1 = new VerificationReplayLogRequest { ArtifactRef = "artifact1" };
|
||||
var request2 = new VerificationReplayLogRequest { ArtifactRef = "artifact2" };
|
||||
|
||||
// Act
|
||||
var log1 = _builder.Build(request1);
|
||||
var log2 = _builder.Build(request2);
|
||||
|
||||
// Assert
|
||||
log1.ReplayId.Should().NotBe(log2.ReplayId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UsesProvidedTimeProvider()
|
||||
{
|
||||
// Arrange
|
||||
var expectedTime = new DateTimeOffset(2026, 1, 29, 12, 0, 0, TimeSpan.Zero);
|
||||
var request = new VerificationReplayLogRequest { ArtifactRef = "test" };
|
||||
|
||||
// Act
|
||||
var log = _builder.Build(request);
|
||||
|
||||
// Assert
|
||||
log.VerifiedAt.Should().Be(expectedTime);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user