using System; using System.Collections.Generic; using System.Linq; using StellaOps.Replay.Core; namespace StellaOps.Scanner.Reachability; /// /// Helper that projects reachability artifacts into the replay manifest. /// public sealed class ReachabilityReplayWriter { /// /// Attaches reachability graphs and runtime traces to the supplied replay manifest. /// public void AttachEvidence( ReplayManifest manifest, IEnumerable? graphs, IEnumerable? traces) { ArgumentNullException.ThrowIfNull(manifest); WriteGraphs(manifest, graphs); WriteTraces(manifest, traces); } private static void WriteGraphs(ReplayManifest manifest, IEnumerable? graphs) { if (graphs is null) { return; } var sanitized = graphs .Where(graph => graph is not null) .Select(graph => NormalizeGraph(graph!)) .Where(graph => graph is not null) .Select(graph => graph!) .DistinctBy(graph => (graph.Kind, graph.CasUri, graph.Sha256, graph.Analyzer, graph.Version)) .OrderBy(graph => graph.CasUri, StringComparer.Ordinal) .ThenBy(graph => graph.Kind, StringComparer.Ordinal) .ToList(); foreach (var graph in sanitized) { manifest.AddReachabilityGraph(new ReplayReachabilityGraphReference { Kind = graph.Kind, CasUri = graph.CasUri, Sha256 = graph.Sha256, HashAlgorithm = string.IsNullOrWhiteSpace(graph.Sha256) ? "sha256" : "blake3-256", Analyzer = graph.Analyzer, Version = graph.Version }); } } private static void WriteTraces(ReplayManifest manifest, IEnumerable? traces) { if (traces is null) { return; } var normalized = traces .Where(trace => trace is not null) .Select(trace => NormalizeTrace(trace!)) .Where(trace => trace is not null) .Select(trace => trace!) .ToList(); var collapsed = normalized .GroupBy(trace => (trace.Source, trace.CasUri, trace.Sha256)) .Select(group => group.OrderBy(t => t.RecordedAt).First()) .OrderBy(trace => trace.RecordedAt) .ThenBy(trace => trace.CasUri, StringComparer.Ordinal) .ToList(); foreach (var trace in collapsed) { manifest.AddReachabilityTrace(new ReplayReachabilityTraceReference { Source = trace.Source, CasUri = trace.CasUri, Sha256 = trace.Sha256, HashAlgorithm = string.IsNullOrWhiteSpace(trace.Sha256) ? "sha256" : "sha256", RecordedAt = trace.RecordedAt }); } } private static NormalizedGraph? NormalizeGraph(ReachabilityReplayGraph graph) { var casUri = Normalize(graph.CasUri); if (string.IsNullOrEmpty(casUri)) { return null; } return new NormalizedGraph( Kind: Normalize(graph.Kind) ?? "static", CasUri: casUri, Sha256: NormalizeHash(graph.Sha256), Analyzer: Normalize(graph.Analyzer) ?? string.Empty, Version: Normalize(graph.Version) ?? string.Empty); } private static NormalizedTrace? NormalizeTrace(ReachabilityReplayTrace trace) { var casUri = Normalize(trace.CasUri); if (string.IsNullOrEmpty(casUri)) { return null; } return new NormalizedTrace( Source: Normalize(trace.Source) ?? string.Empty, CasUri: casUri, Sha256: NormalizeHash(trace.Sha256), RecordedAt: trace.RecordedAt.ToUniversalTime()); } private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); private static string NormalizeHash(string? hash) => string.IsNullOrWhiteSpace(hash) ? string.Empty : hash.Trim().ToLowerInvariant(); private sealed record NormalizedGraph( string Kind, string CasUri, string Sha256, string Analyzer, string Version); private sealed record NormalizedTrace( string Source, string CasUri, string Sha256, DateTimeOffset RecordedAt); } /// /// Describes a CAS-backed reachability graph emitted by Scanner. /// public sealed record ReachabilityReplayGraph( string? Kind, string? CasUri, string? Sha256, string? Analyzer, string? Version); /// /// Describes a runtime trace artifact emitted by Scanner/Zastava. /// public sealed record ReachabilityReplayTrace( string? Source, string? CasUri, string? Sha256, DateTimeOffset RecordedAt);