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
227 lines
7.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|