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:
2025-10-19 18:36:22 +03:00
parent 7e2fa0a42a
commit 5ce40d2eeb
966 changed files with 91038 additions and 1850 deletions

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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>