using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using StellaOps.Zastava.Core.Contracts; using StellaOps.Zastava.Observer.Configuration; using StellaOps.Zastava.Observer.ContainerRuntime; using StellaOps.Zastava.Observer.ContainerRuntime.Cri; using StellaOps.Zastava.Observer.Runtime; namespace StellaOps.Zastava.Observer.Worker; internal static class RuntimeEventFactory { public static RuntimeEventEnvelope Create( ContainerLifecycleEvent lifecycleEvent, ContainerRuntimeEndpointOptions endpoint, CriRuntimeIdentity identity, string tenant, string nodeName, RuntimeProcessCapture? capture = null, RuntimePosture? posture = null, IReadOnlyList? additionalEvidence = null) { ArgumentNullException.ThrowIfNull(lifecycleEvent); ArgumentNullException.ThrowIfNull(endpoint); ArgumentNullException.ThrowIfNull(identity); ArgumentNullException.ThrowIfNull(tenant); ArgumentNullException.ThrowIfNull(nodeName); var snapshot = lifecycleEvent.Snapshot; var workloadLabels = snapshot.Labels ?? new Dictionary(StringComparer.Ordinal); var annotations = snapshot.Annotations is null ? new Dictionary(StringComparer.Ordinal) : new Dictionary(snapshot.Annotations, StringComparer.Ordinal); var platform = ResolvePlatform(workloadLabels, endpoint); var runtimeEvent = new RuntimeEvent { EventId = ComputeEventId(nodeName, lifecycleEvent), When = lifecycleEvent.Timestamp, Kind = lifecycleEvent.Kind == ContainerLifecycleEventKind.Start ? RuntimeEventKind.ContainerStart : RuntimeEventKind.ContainerStop, Tenant = tenant, Node = nodeName, Runtime = new RuntimeEngine { Engine = endpoint.Engine.ToEngineString(), Version = identity.RuntimeVersion }, Workload = new RuntimeWorkload { Platform = platform, Namespace = TryGet(workloadLabels, CriLabelKeys.PodNamespace), Pod = TryGet(workloadLabels, CriLabelKeys.PodName), Container = TryGet(workloadLabels, CriLabelKeys.ContainerName) ?? snapshot.Name, ContainerId = $"{endpoint.Engine.ToEngineString()}://{snapshot.Id}", ImageRef = ResolveImageRef(snapshot), Owner = null }, Process = capture?.Process, LoadedLibraries = capture?.Libraries ?? Array.Empty(), Posture = posture, Evidence = MergeEvidence(capture?.Evidence, additionalEvidence), Annotations = annotations.Count == 0 ? null : new SortedDictionary(annotations, StringComparer.Ordinal) }; return RuntimeEventEnvelope.Create(runtimeEvent, ZastavaContractVersions.RuntimeEvent); } private static string ResolvePlatform(IReadOnlyDictionary labels, ContainerRuntimeEndpointOptions endpoint) { if (labels.ContainsKey(CriLabelKeys.PodName)) { return "kubernetes"; } return endpoint.Engine.ToEngineString(); } private static IReadOnlyList MergeEvidence( IReadOnlyList? primary, IReadOnlyList? secondary) { if ((primary is null || primary.Count == 0) && (secondary is null || secondary.Count == 0)) { return Array.Empty(); } if (secondary is null || secondary.Count == 0) { return primary ?? Array.Empty(); } if (primary is null || primary.Count == 0) { return secondary; } var merged = new List(primary.Count + secondary.Count); merged.AddRange(primary); merged.AddRange(secondary); return merged; } private static string? ResolveImageRef(CriContainerInfo snapshot) { if (!string.IsNullOrWhiteSpace(snapshot.ImageRef)) { return snapshot.ImageRef; } return snapshot.Image; } private static string? TryGet(IReadOnlyDictionary dictionary, string key) { if (dictionary.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) { return value; } return null; } private static string ComputeEventId(string nodeName, ContainerLifecycleEvent lifecycleEvent) { var builder = new StringBuilder() .Append(nodeName) .Append('|') .Append(lifecycleEvent.Snapshot.Id) .Append('|') .Append(lifecycleEvent.Timestamp.ToUniversalTime().Ticks) .Append('|') .Append((int)lifecycleEvent.Kind); var bytes = Encoding.UTF8.GetBytes(builder.ToString()); Span hash = stackalloc byte[16]; if (!MD5.TryHashData(bytes, hash, out _)) { using var md5 = MD5.Create(); hash = md5.ComputeHash(bytes).AsSpan(0, 16); } var guid = new Guid(hash); return guid.ToString("N"); } }