using System.Text; using StellaOps.Zastava.Core.Contracts; using StellaOps.Zastava.Core.Hashing; using StellaOps.Zastava.Core.Serialization; namespace StellaOps.Zastava.Core.Tests.Serialization; public sealed class ZastavaCanonicalJsonSerializerTests { [Fact] public void Serialize_RuntimeEventEnvelope_ProducesDeterministicOrdering() { var runtimeEvent = new RuntimeEvent { EventId = "evt-123", When = DateTimeOffset.Parse("2025-10-19T12:34:56Z"), Kind = RuntimeEventKind.ContainerStart, Tenant = "tenant-01", Node = "node-a", Runtime = new RuntimeEngine { Engine = "containerd", Version = "1.7.19" }, Workload = new RuntimeWorkload { Platform = "kubernetes", Namespace = "payments", Pod = "api-7c9fbbd8b7-ktd84", Container = "api", ContainerId = "containerd://abc", ImageRef = "ghcr.io/acme/api@sha256:abcd", Owner = new RuntimeWorkloadOwner { Kind = "Deployment", Name = "api" } }, Process = new RuntimeProcess { Pid = 12345, Entrypoint = new[] { "/entrypoint.sh", "--serve" }, EntryTrace = new[] { new RuntimeEntryTrace { File = "/entrypoint.sh", Line = 3, Op = "exec", Target = "/usr/bin/python3" } } }, LoadedLibraries = new[] { new RuntimeLoadedLibrary { Path = "/lib/x86_64-linux-gnu/libssl.so.3", Inode = 123456, Sha256 = "abc123" } }, Posture = new RuntimePosture { ImageSigned = true, SbomReferrer = "present", Attestation = new RuntimeAttestation { Uuid = "rekor-uuid", Verified = true } }, Delta = new RuntimeDelta { BaselineImageDigest = "sha256:abcd", ChangedFiles = new[] { "/opt/app/server.py" }, NewBinaries = new[] { new RuntimeNewBinary { Path = "/usr/local/bin/helper", Sha256 = "def456" } } }, Evidence = new[] { new RuntimeEvidence { Signal = "procfs.maps", Value = "/lib/.../libssl.so.3@0x7f..." } }, Annotations = new Dictionary { ["source"] = "unit-test" } }; var envelope = RuntimeEventEnvelope.Create(runtimeEvent, ZastavaContractVersions.RuntimeEvent); var json = ZastavaCanonicalJsonSerializer.Serialize(envelope); var expectedOrder = new[] { "\"schemaVersion\"", "\"event\"", "\"eventId\"", "\"when\"", "\"kind\"", "\"tenant\"", "\"node\"", "\"runtime\"", "\"engine\"", "\"version\"", "\"workload\"", "\"platform\"", "\"namespace\"", "\"pod\"", "\"container\"", "\"containerId\"", "\"imageRef\"", "\"owner\"", "\"kind\"", "\"name\"", "\"process\"", "\"pid\"", "\"entrypoint\"", "\"entryTrace\"", "\"loadedLibs\"", "\"posture\"", "\"imageSigned\"", "\"sbomReferrer\"", "\"attestation\"", "\"uuid\"", "\"verified\"", "\"delta\"", "\"baselineImageDigest\"", "\"changedFiles\"", "\"newBinaries\"", "\"path\"", "\"sha256\"", "\"evidence\"", "\"signal\"", "\"value\"", "\"annotations\"", "\"source\"" }; var cursor = -1; foreach (var token in expectedOrder) { var position = json.IndexOf(token, cursor + 1, StringComparison.Ordinal); Assert.True(position > cursor, $"Property token {token} not found in the expected order."); cursor = position; } Assert.DoesNotContain(" ", json, StringComparison.Ordinal); Assert.StartsWith("{\"schemaVersion\"", json, StringComparison.Ordinal); Assert.EndsWith("}}", json, StringComparison.Ordinal); } [Fact] public void ComputeMultihash_ProducesStableBase64UrlDigest() { var decision = AdmissionDecisionEnvelope.Create( new AdmissionDecision { AdmissionId = "admission-123", Namespace = "payments", PodSpecDigest = "sha256:deadbeef", Images = new[] { new AdmissionImageVerdict { Name = "ghcr.io/acme/api:1.2.3", Resolved = "ghcr.io/acme/api@sha256:abcd", Signed = true, HasSbomReferrers = true, PolicyVerdict = PolicyVerdict.Pass, Reasons = Array.Empty(), Rekor = new AdmissionRekorEvidence { Uuid = "xyz", Verified = true } } }, Decision = AdmissionDecisionOutcome.Allow, TtlSeconds = 300 }, ZastavaContractVersions.AdmissionDecision); var canonicalJson = ZastavaCanonicalJsonSerializer.Serialize(decision); var expectedDigestBytes = SHA256.HashData(Encoding.UTF8.GetBytes(canonicalJson)); var expected = $"sha256-{Convert.ToBase64String(expectedDigestBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')}"; var hash = ZastavaHashing.ComputeMultihash(decision); Assert.Equal(expected, hash); var sha512 = ZastavaHashing.ComputeMultihash(Encoding.UTF8.GetBytes(canonicalJson), "sha512"); Assert.StartsWith("sha512-", sha512, StringComparison.Ordinal); } }