using StellaOps.Zastava.Core.Contracts; using StellaOps.Zastava.Observer.Configuration; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; namespace StellaOps.Zastava.Observer.Runtime; internal static class RuntimeFactsBuilder { public static RuntimeFactsPublishRequest? Build( IReadOnlyCollection envelopes, ReachabilityRuntimeOptions options) { ArgumentNullException.ThrowIfNull(envelopes); ArgumentNullException.ThrowIfNull(options); if (!options.Enabled || envelopes.Count == 0) { return null; } if (string.IsNullOrWhiteSpace(options.CallgraphId)) { return null; } var facts = new List(envelopes.Count); ReachabilitySubjectPayload? subject = null; foreach (var envelope in envelopes) { var fact = TryBuildFact(envelope); if (fact is null) { continue; } facts.Add(fact); subject ??= BuildSubject(envelope, options); } if (facts.Count == 0) { return null; } subject ??= BuildFallbackSubject(options); if (subject is null) { return null; } facts.Sort(CompareFacts); return new RuntimeFactsPublishRequest { CallgraphId = options.CallgraphId.Trim(), Subject = subject, Events = facts }; } private static RuntimeFactEventPayload? TryBuildFact(RuntimeEventEnvelope envelope) { ArgumentNullException.ThrowIfNull(envelope); var evt = envelope.Event; if (evt.Kind != RuntimeEventKind.ContainerStart || evt.Process is null) { return null; } var symbolId = BuildSymbolId(evt.Process); if (string.IsNullOrWhiteSpace(symbolId)) { return null; } var fact = new RuntimeFactEventPayload { SymbolId = symbolId, CodeId = Normalize(evt.Process.BuildId), BuildId = Normalize(evt.Process.BuildId), LoaderBase = null, Purl = null, SymbolDigest = null, HitCount = 1, ObservedAt = evt.When, ProcessId = evt.Process.Pid, ProcessName = ResolveProcessName(evt.Process.Entrypoint), ContainerId = evt.Workload.ContainerId, Metadata = BuildMetadata(evt.Process) }; return fact; } private static string? BuildSymbolId(RuntimeProcess process) { if (!string.IsNullOrWhiteSpace(process.BuildId)) { return $"sym:binary:{process.BuildId.Trim().ToLowerInvariant()}"; } var trace = process.EntryTrace?.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t.Target) || !string.IsNullOrWhiteSpace(t.File)); var seed = trace?.Target ?? trace?.File ?? process.Entrypoint.FirstOrDefault(); if (string.IsNullOrWhiteSpace(seed)) { return null; } var stable = ComputeStableFragment(seed); return $"sym:shell:{stable}"; } private static ReachabilitySubjectPayload? BuildSubject(RuntimeEventEnvelope envelope, ReachabilityRuntimeOptions options) { var workload = envelope.Event.Workload; var imageRef = workload.ImageRef; var digest = ExtractImageDigest(imageRef); var (component, version) = ExtractComponentAndVersion(imageRef); digest ??= Normalize(options.SubjectImageDigest); component ??= Normalize(options.SubjectComponent); version ??= Normalize(options.SubjectVersion); var scanId = Normalize(options.SubjectScanId); if (string.IsNullOrWhiteSpace(digest) && string.IsNullOrWhiteSpace(scanId) && (string.IsNullOrWhiteSpace(component) || string.IsNullOrWhiteSpace(version))) { return null; } return new ReachabilitySubjectPayload { ScanId = scanId, ImageDigest = digest, Component = component, Version = version }; } private static ReachabilitySubjectPayload? BuildFallbackSubject(ReachabilityRuntimeOptions options) { var digest = Normalize(options.SubjectImageDigest); var component = Normalize(options.SubjectComponent); var version = Normalize(options.SubjectVersion); var scanId = Normalize(options.SubjectScanId); if (string.IsNullOrWhiteSpace(digest) && string.IsNullOrWhiteSpace(scanId) && (string.IsNullOrWhiteSpace(component) || string.IsNullOrWhiteSpace(version))) { return null; } return new ReachabilitySubjectPayload { ScanId = scanId, ImageDigest = digest, Component = component, Version = version }; } private static Dictionary? BuildMetadata(RuntimeProcess process) { if (process.EntryTrace is null || process.EntryTrace.Count == 0) { return null; } var sb = new StringBuilder(); foreach (var trace in process.EntryTrace) { if (trace is null) { continue; } var op = string.IsNullOrWhiteSpace(trace.Op) ? "exec" : trace.Op; var target = trace.Target ?? trace.File; if (string.IsNullOrWhiteSpace(target)) { continue; } if (sb.Length > 0) { sb.Append(" | "); } sb.Append(op).Append(':').Append(target); } if (sb.Length == 0) { return null; } return new Dictionary(StringComparer.Ordinal) { ["entryTrace"] = sb.ToString() }; } private static string? ExtractImageDigest(string? imageRef) { if (string.IsNullOrWhiteSpace(imageRef)) { return null; } var digestStart = imageRef.IndexOf("sha256:", StringComparison.OrdinalIgnoreCase); if (digestStart >= 0) { return imageRef[digestStart..].Trim(); } var atIndex = imageRef.IndexOf('@'); if (atIndex > 0 && atIndex + 1 < imageRef.Length) { return imageRef[(atIndex + 1)..].Trim(); } return null; } private static (string? Component, string? Version) ExtractComponentAndVersion(string? imageRef) { if (string.IsNullOrWhiteSpace(imageRef)) { return (null, null); } var lastSlash = imageRef.LastIndexOf('/'); var lastColon = imageRef.LastIndexOf(':'); if (lastColon < 0 || lastColon < lastSlash) { return (null, null); } var component = imageRef[(lastSlash + 1)..lastColon]; var version = imageRef[(lastColon + 1)..]; if (string.IsNullOrWhiteSpace(component) || string.IsNullOrWhiteSpace(version)) { return (null, null); } return (component.Trim(), version.Trim()); } private static string? ResolveProcessName(IReadOnlyList? entrypoint) { if (entrypoint is null || entrypoint.Count == 0) { return null; } var first = entrypoint[0]; if (string.IsNullOrWhiteSpace(first)) { return null; } var lastSlash = first.LastIndexOf('/'); return lastSlash >= 0 ? first[(lastSlash + 1)..] : first; } private static string ComputeStableFragment(string seed) { var normalized = seed.Trim().ToLowerInvariant(); var bytes = Encoding.UTF8.GetBytes(normalized); Span hash = stackalloc byte[32]; _ = SHA256.TryHashData(bytes, hash, out _); return Convert.ToHexString(hash[..16]).ToLowerInvariant(); } private static int CompareFacts(RuntimeFactEventPayload left, RuntimeFactEventPayload right) { var timeComparison = Nullable.Compare(left.ObservedAt, right.ObservedAt); if (timeComparison != 0) { return timeComparison; } return string.Compare(left.SymbolId, right.SymbolId, StringComparison.Ordinal); } private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } internal sealed class RuntimeFactsPublishRequest { public ReachabilitySubjectPayload Subject { get; set; } = new(); public string CallgraphId { get; set; } = string.Empty; public List Events { get; set; } = new(); } internal sealed class ReachabilitySubjectPayload { public string? ScanId { get; set; } public string? ImageDigest { get; set; } public string? Component { get; set; } public string? Version { get; set; } } internal sealed class RuntimeFactEventPayload { public string SymbolId { get; set; } = string.Empty; public string? CodeId { get; set; } public string? SymbolDigest { get; set; } public string? Purl { get; set; } public string? BuildId { get; set; } public string? LoaderBase { get; set; } public int? ProcessId { get; set; } public string? ProcessName { get; set; } public string? SocketAddress { get; set; } public string? ContainerId { get; set; } public string? EvidenceUri { get; set; } public int HitCount { get; set; } = 1; public DateTimeOffset? ObservedAt { get; set; } public Dictionary? Metadata { get; set; } } internal sealed class RuntimeFactsException : Exception { public RuntimeFactsException(string message, Exception? innerException = null) : base(message, innerException) { } }