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
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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user