feat: Initialize Zastava Webhook service with TLS and Authority authentication
- 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.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Zastava.Core.Tests.Contracts;
|
||||
|
||||
public sealed class ZastavaContractVersionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("zastava.runtime.event@v1", "zastava.runtime.event", 1, 0)]
|
||||
[InlineData("zastava.runtime.event@v1.0", "zastava.runtime.event", 1, 0)]
|
||||
[InlineData("zastava.admission.decision@v1.2", "zastava.admission.decision", 1, 2)]
|
||||
public void TryParse_ParsesCanonicalForms(string input, string schema, int major, int minor)
|
||||
{
|
||||
var success = ZastavaContractVersions.ContractVersion.TryParse(input, out var contract);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.Equal(schema, contract.Schema);
|
||||
Assert.Equal(new Version(major, minor), contract.Version);
|
||||
Assert.Equal($"{schema}@v{major}.{minor}", contract.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("zastava.runtime.event")]
|
||||
[InlineData("runtime@1.0")]
|
||||
[InlineData("zastava.runtime.event@vinvalid")]
|
||||
public void TryParse_InvalidInputs_ReturnsFalse(string input)
|
||||
{
|
||||
var success = ZastavaContractVersions.ContractVersion.TryParse(input, out _);
|
||||
|
||||
Assert.False(success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRuntimeEventSupported_RespectsMajorCompatibility()
|
||||
{
|
||||
Assert.True(ZastavaContractVersions.IsRuntimeEventSupported("zastava.runtime.event@v1"));
|
||||
Assert.True(ZastavaContractVersions.IsRuntimeEventSupported("zastava.runtime.event@v1.0"));
|
||||
Assert.False(ZastavaContractVersions.IsRuntimeEventSupported("zastava.runtime.event@v2.0"));
|
||||
Assert.False(ZastavaContractVersions.IsRuntimeEventSupported("zastava.admission.decision@v1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegotiateRuntimeEvent_PicksHighestCommonVersion()
|
||||
{
|
||||
var negotiated = ZastavaContractVersions.NegotiateRuntimeEvent(new[]
|
||||
{
|
||||
"zastava.runtime.event@v1.0",
|
||||
"zastava.runtime.event@v0.9",
|
||||
"zastava.admission.decision@v1"
|
||||
});
|
||||
|
||||
Assert.Equal("zastava.runtime.event@v1.0", negotiated.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegotiateRuntimeEvent_FallsBackToLocalWhenNoMatch()
|
||||
{
|
||||
var negotiated = ZastavaContractVersions.NegotiateRuntimeEvent(new[]
|
||||
{
|
||||
"zastava.runtime.event@v2.0",
|
||||
"zastava.admission.decision@v2.0"
|
||||
});
|
||||
|
||||
Assert.Equal(ZastavaContractVersions.RuntimeEvent.ToString(), negotiated.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user