using System.Buffers; using System.Linq; using System.Text.Encodings.Web; 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; internal sealed class AdmissionResponseBuilder { public (AdmissionDecisionEnvelope Envelope, AdmissionReviewResponseDto Response) Build( AdmissionRequestContext context, RuntimeAdmissionEvaluation evaluation) { var decision = BuildDecision(context, evaluation); var envelope = AdmissionDecisionEnvelope.Create(decision, ZastavaContractVersions.AdmissionDecision); ZastavaContractValidator.ValidateAdmissionDecision(envelope); var auditAnnotations = CreateAuditAnnotations(envelope, evaluation); var warnings = BuildWarnings(evaluation); var allowed = evaluation.Decisions.All(static d => d.Allowed); var status = allowed ? null : new AdmissionReviewStatus { Code = 403, Message = BuildFailureMessage(evaluation) }; var response = new AdmissionReviewResponseDto { ApiVersion = context.ApiVersion, Kind = context.Kind, Response = new AdmissionReviewResponsePayload { Uid = context.Uid, Allowed = allowed, Status = status, Warnings = warnings, AuditAnnotations = auditAnnotations } }; return (envelope, response); } private static AdmissionDecision BuildDecision(AdmissionRequestContext context, RuntimeAdmissionEvaluation evaluation) { var images = new List(evaluation.Decisions.Count); for (var i = 0; i < evaluation.Decisions.Count; i++) { var decision = evaluation.Decisions[i]; var container = context.Containers[Math.Min(i, context.Containers.Count - 1)]; var metadata = new Dictionary(StringComparer.Ordinal) { ["image"] = decision.OriginalImage }; if (!string.Equals(container.Image, container.Name, StringComparison.Ordinal)) { metadata["container"] = container.Name; } if (decision.FromCache) { metadata["cache"] = "hit"; } if (!string.IsNullOrWhiteSpace(decision.SurfacePointer)) { metadata["surfacePointer"] = decision.SurfacePointer!; } var resolved = decision.ResolvedDigest ?? decision.OriginalImage; images.Add(new AdmissionImageVerdict { Name = container.Name, Resolved = resolved, Signed = decision.Policy?.Signed ?? false, HasSbomReferrers = decision.Policy?.HasSbom ?? false, PolicyVerdict = decision.Verdict, Reasons = decision.Reasons, Rekor = decision.Policy?.Rekor, Metadata = metadata }); } return new AdmissionDecision { AdmissionId = context.Uid, Namespace = context.Namespace, PodSpecDigest = ComputePodSpecDigest(context.PodSpec), Images = images, Decision = evaluation.Decisions.All(static d => d.Allowed) ? AdmissionDecisionOutcome.Allow : AdmissionDecisionOutcome.Deny, TtlSeconds = Math.Max(0, evaluation.TtlSeconds), Annotations = BuildAnnotations(evaluation) }; } private static IReadOnlyDictionary? BuildAnnotations(RuntimeAdmissionEvaluation evaluation) { if (!evaluation.BackendFailed && !evaluation.FailOpenApplied && evaluation.FailureReason is null) { return null; } var annotations = new Dictionary(StringComparer.Ordinal); if (evaluation.BackendFailed) { annotations["zastava.backend.failed"] = "true"; } if (evaluation.FailOpenApplied) { annotations["zastava.failOpen"] = "true"; } if (!string.IsNullOrWhiteSpace(evaluation.FailureReason)) { annotations["zastava.failureReason"] = evaluation.FailureReason!; } return annotations; } private static IReadOnlyDictionary CreateAuditAnnotations(AdmissionDecisionEnvelope envelope, RuntimeAdmissionEvaluation evaluation) { var annotations = new Dictionary(StringComparer.Ordinal) { ["zastava.stellaops/admission"] = ZastavaCanonicalJsonSerializer.Serialize(envelope) }; if (evaluation.FailOpenApplied) { annotations["zastava.stellaops/failOpen"] = "true"; } return annotations; } private static IReadOnlyList? BuildWarnings(RuntimeAdmissionEvaluation evaluation) { var warnings = new List(); if (evaluation.FailOpenApplied) { warnings.Add("zastava.fail_open.applied"); } foreach (var decision in evaluation.Decisions) { if (decision.Verdict == PolicyVerdict.Warn) { warnings.Add($"policy.warn:{decision.OriginalImage}"); } } return warnings.Count == 0 ? null : warnings; } private static string BuildFailureMessage(RuntimeAdmissionEvaluation evaluation) { if (!string.IsNullOrWhiteSpace(evaluation.FailureReason)) { return evaluation.FailureReason!; } var denied = evaluation.Decisions .Where(static d => !d.Allowed) .SelectMany(static d => d.Reasons) .Distinct(StringComparer.Ordinal) .ToArray(); return denied.Length > 0 ? string.Join(", ", denied) : "admission.denied"; } private static string ComputePodSpecDigest(JsonElement podSpec) { var buffer = new ArrayBufferWriter(); using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Indented = false })) { WriteCanonical(podSpec, writer); } return ZastavaHashing.ComputeMultihash(buffer.WrittenSpan); } private static void WriteCanonical(JsonElement element, Utf8JsonWriter writer) { switch (element.ValueKind) { case JsonValueKind.Object: writer.WriteStartObject(); foreach (var property in element.EnumerateObject().OrderBy(static p => p.Name, StringComparer.Ordinal)) { writer.WritePropertyName(property.Name); WriteCanonical(property.Value, writer); } writer.WriteEndObject(); break; case JsonValueKind.Array: writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) { WriteCanonical(item, writer); } writer.WriteEndArray(); break; default: element.WriteTo(writer); break; } } }