up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-03 00:10:19 +02:00
parent ea1d58a89b
commit 37cba83708
158 changed files with 147438 additions and 867 deletions

View File

@@ -6,6 +6,7 @@ using StellaOps.Zastava.Observer.Configuration;
using StellaOps.Zastava.Observer.ContainerRuntime;
using StellaOps.Zastava.Observer.ContainerRuntime.Cri;
using StellaOps.Zastava.Observer.Runtime;
using StellaOps.Zastava.Core.Security;
namespace StellaOps.Zastava.Observer.Worker;
@@ -65,7 +66,9 @@ internal static class RuntimeEventFactory
Annotations = annotations.Count == 0 ? null : new SortedDictionary<string, string>(annotations, StringComparer.Ordinal)
};
return RuntimeEventEnvelope.Create(runtimeEvent, ZastavaContractVersions.RuntimeEvent);
var envelope = RuntimeEventEnvelope.Create(runtimeEvent, ZastavaContractVersions.RuntimeEvent);
ZastavaContractValidator.ValidateRuntimeEvent(envelope);
return envelope;
}
private static string ResolvePlatform(IReadOnlyDictionary<string, string> labels, ContainerRuntimeEndpointOptions endpoint)

View File

@@ -5,6 +5,7 @@ using System.Text.Json;
using StellaOps.Zastava.Core.Contracts;
using StellaOps.Zastava.Core.Hashing;
using StellaOps.Zastava.Core.Serialization;
using StellaOps.Zastava.Core.Security;
namespace StellaOps.Zastava.Webhook.Admission;
@@ -16,6 +17,7 @@ internal sealed class AdmissionResponseBuilder
{
var decision = BuildDecision(context, evaluation);
var envelope = AdmissionDecisionEnvelope.Create(decision, ZastavaContractVersions.AdmissionDecision);
ZastavaContractValidator.ValidateAdmissionDecision(envelope);
var auditAnnotations = CreateAuditAnnotations(envelope, evaluation);
var warnings = BuildWarnings(evaluation);

View File

@@ -0,0 +1,51 @@
using StellaOps.Zastava.Core.Contracts;
namespace StellaOps.Zastava.Core.Security;
/// <summary>
/// Lightweight, deterministic guards for Zastava runtime and admission contracts.
/// </summary>
public static class ZastavaContractValidator
{
public static void ValidateRuntimeEvent(RuntimeEventEnvelope envelope)
{
ArgumentNullException.ThrowIfNull(envelope);
ArgumentNullException.ThrowIfNull(envelope.Event);
if (string.IsNullOrWhiteSpace(envelope.Event.Tenant))
{
throw new ArgumentException("tenant must be set on runtime events", nameof(envelope));
}
if (envelope.Event.When.Offset != TimeSpan.Zero)
{
throw new ArgumentException("runtime event timestamps must be UTC", nameof(envelope));
}
if (string.IsNullOrWhiteSpace(envelope.Event.EventId))
{
throw new ArgumentException("runtime event ID must be populated", nameof(envelope));
}
}
public static void ValidateAdmissionDecision(AdmissionDecisionEnvelope envelope)
{
ArgumentNullException.ThrowIfNull(envelope);
ArgumentNullException.ThrowIfNull(envelope.Decision);
if (string.IsNullOrWhiteSpace(envelope.Decision.Namespace))
{
throw new ArgumentException("admission namespace is required", nameof(envelope));
}
if (string.IsNullOrWhiteSpace(envelope.Decision.PodSpecDigest))
{
throw new ArgumentException("podSpecDigest must be set on admission decisions", nameof(envelope));
}
if (envelope.Decision.Images is null || envelope.Decision.Images.Count == 0)
{
throw new ArgumentException("at least one image verdict is required", nameof(envelope));
}
}
}

View File

@@ -13,8 +13,8 @@ namespace StellaOps.Zastava.Observer.Tests.Worker;
public sealed class RuntimeEventFactoryTests
{
[Fact]
public void Create_AttachesBuildIdFromProcessCapture()
{
public void Create_AttachesBuildIdFromProcessCapture()
{
var timestamp = DateTimeOffset.UtcNow;
var snapshot = new CriContainerInfo(
Id: "container-a",
@@ -68,7 +68,45 @@ public sealed class RuntimeEventFactoryTests
posture: null,
additionalEvidence: null);
Assert.NotNull(envelope.Event.Process);
Assert.Equal("5f0c7c3cb4d9f8a4", envelope.Event.Process!.BuildId);
}
}
Assert.NotNull(envelope.Event.Process);
Assert.Equal("5f0c7c3cb4d9f8a4", envelope.Event.Process!.BuildId);
}
[Fact]
public void Create_ThrowsWhenTenantMissing()
{
var timestamp = DateTimeOffset.UtcNow;
var snapshot = new CriContainerInfo(
Id: "container-b",
PodSandboxId: "sandbox-b",
Name: "api",
Attempt: 1,
Image: "ghcr.io/example/api:1.0",
ImageRef: "ghcr.io/example/api@sha256:deadbeef",
Labels: new Dictionary<string, string>(),
Annotations: new Dictionary<string, string>(),
CreatedAt: timestamp,
StartedAt: timestamp,
FinishedAt: null,
ExitCode: null,
Reason: null,
Message: null,
Pid: 1111);
var lifecycleEvent = new ContainerLifecycleEvent(ContainerLifecycleEventKind.Start, timestamp, snapshot);
var endpoint = new ContainerRuntimeEndpointOptions
{
Engine = ContainerRuntimeEngine.Containerd,
Endpoint = "unix:///run/containerd/containerd.sock",
Name = "containerd"
};
var identity = new CriRuntimeIdentity("containerd", "1.7.19", "v1");
Assert.Throws<ArgumentException>(() => RuntimeEventFactory.Create(
lifecycleEvent,
endpoint,
identity,
tenant: " ",
nodeName: "node-1"));
}
}

View File

@@ -132,4 +132,60 @@ public sealed class AdmissionResponseBuilderTests
Assert.NotNull(response.Response.AuditAnnotations);
Assert.Contains("zastava.stellaops/admission", response.Response.AuditAnnotations!.Keys);
}
[Fact]
public void Build_ThrowsWhenNamespaceMissing()
{
using var document = JsonDocument.Parse("""
{
"metadata": { "namespace": "" },
"spec": {
"containers": [ { "name": "api", "image": "ghcr.io/example/api:1.0" } ]
}
}
""");
var pod = document.RootElement;
var spec = pod.GetProperty("spec");
var context = new AdmissionRequestContext(
ApiVersion: "admission.k8s.io/v1",
Kind: "AdmissionReview",
Uid: "abc",
Namespace: string.Empty,
Labels: new Dictionary<string, string>(),
Containers: new[] { new AdmissionContainerReference("api", "ghcr.io/example/api:1.0") },
PodObject: pod,
PodSpec: spec);
var evaluation = new RuntimeAdmissionEvaluation
{
Decisions = new[]
{
new RuntimeAdmissionDecision
{
OriginalImage = "ghcr.io/example/api:1.0",
ResolvedDigest = "ghcr.io/example/api@sha256:deadbeef",
Verdict = PolicyVerdict.Pass,
Allowed = true,
Policy = new RuntimePolicyImageResult
{
PolicyVerdict = PolicyVerdict.Pass,
HasSbom = true,
Signed = true
},
Reasons = Array.Empty<string>(),
FromCache = false,
ResolutionFailed = false
}
},
BackendFailed = false,
FailOpenApplied = false,
FailureReason = null,
TtlSeconds = 300
};
var builder = new AdmissionResponseBuilder();
Assert.Throws<ArgumentException>(() => builder.Build(context, evaluation));
}
}