Files
git.stella-ops.org/src/Zastava/StellaOps.Zastava.Webhook/Admission/AdmissionResponseBuilder.cs
StellaOps Bot 37cba83708
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
up
2025-12-03 00:10:19 +02:00

227 lines
7.4 KiB
C#

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<AdmissionImageVerdict>(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<string, string>(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<string, string>? BuildAnnotations(RuntimeAdmissionEvaluation evaluation)
{
if (!evaluation.BackendFailed && !evaluation.FailOpenApplied && evaluation.FailureReason is null)
{
return null;
}
var annotations = new Dictionary<string, string>(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<string, string> CreateAuditAnnotations(AdmissionDecisionEnvelope envelope, RuntimeAdmissionEvaluation evaluation)
{
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
{
["zastava.stellaops/admission"] = ZastavaCanonicalJsonSerializer.Serialize(envelope)
};
if (evaluation.FailOpenApplied)
{
annotations["zastava.stellaops/failOpen"] = "true";
}
return annotations;
}
private static IReadOnlyList<string>? BuildWarnings(RuntimeAdmissionEvaluation evaluation)
{
var warnings = new List<string>();
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<byte>();
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;
}
}
}