- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
205 lines
6.7 KiB
C#
205 lines
6.7 KiB
C#
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<string, string>
|
|
{
|
|
["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<string>(),
|
|
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);
|
|
}
|
|
}
|