Add tests for SBOM generation determinism across multiple formats

- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism.
- Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions.
- Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests.
- Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
This commit is contained in:
master
2025-12-23 18:56:12 +02:00
committed by StellaOps Bot
parent 7ac70ece71
commit 491e883653
409 changed files with 23797 additions and 17779 deletions

View File

@@ -1,5 +1,7 @@
namespace StellaOps.AirGap.Importer.Reconciliation;
using StellaOps.Cryptography.Digests;
/// <summary>
/// Digest-keyed artifact index used by the evidence reconciliation flow.
/// Designed for deterministic ordering and replay.
@@ -39,54 +41,7 @@ public sealed class ArtifactIndex
public IEnumerable<KeyValuePair<string, ArtifactEntry>> GetAll() => _entries;
public static string NormalizeDigest(string digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
throw new ArgumentException("Digest is required.", nameof(digest));
}
digest = digest.Trim();
const string prefix = "sha256:";
string hex;
if (digest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
hex = digest[prefix.Length..];
}
else if (digest.Contains(':', StringComparison.Ordinal))
{
throw new FormatException($"Unsupported digest algorithm in '{digest}'. Only sha256 is supported.");
}
else
{
hex = digest;
}
hex = hex.Trim().ToLowerInvariant();
if (hex.Length != 64 || !IsLowerHex(hex.AsSpan()))
{
throw new FormatException($"Invalid sha256 digest '{digest}'. Expected 64 hex characters.");
}
return prefix + hex;
}
private static bool IsLowerHex(ReadOnlySpan<char> value)
{
foreach (var c in value)
{
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))
{
continue;
}
return false;
}
return true;
}
=> Sha256Digest.Normalize(digest, requirePrefix: false, parameterName: nameof(digest));
}
public sealed record ArtifactEntry(

View File

@@ -0,0 +1,43 @@
using StellaOps.AirGap.Importer.Reconciliation;
namespace StellaOps.AirGap.Importer.Tests.Reconciliation;
public sealed class ArtifactIndexDigestNormalizationTests
{
[Fact]
public void NormalizeDigest_AcceptsBareHex()
{
var digest = new string('A', 64);
var normalized = ArtifactIndex.NormalizeDigest(digest);
Assert.Equal("sha256:" + new string('a', 64), normalized);
}
[Fact]
public void NormalizeDigest_AcceptsPrefixedSha256()
{
var digest = "SHA256:" + new string('F', 64);
var normalized = ArtifactIndex.NormalizeDigest(digest);
Assert.Equal("sha256:" + new string('f', 64), normalized);
}
[Fact]
public void NormalizeDigest_RejectsUnsupportedAlgorithm()
{
var digest = "sha512:" + new string('a', 128);
Assert.Throws<FormatException>(() => ArtifactIndex.NormalizeDigest(digest));
}
[Fact]
public void NormalizeDigest_RejectsNonHex()
{
var digest = "sha256:" + new string('g', 64);
Assert.Throws<FormatException>(() => ArtifactIndex.NormalizeDigest(digest));
}
}