// Licensed to StellaOps under the AGPL-3.0-or-later license. using System.Diagnostics; using StellaOps.ReachGraph.Hashing; using StellaOps.ReachGraph.Persistence; using StellaOps.ReachGraph.WebService.Models; namespace StellaOps.ReachGraph.WebService.Services; /// /// Service for verifying deterministic replay of reachability graphs. /// public sealed class ReachGraphReplayService : IReachGraphReplayService { private readonly IReachGraphStoreService _storeService; private readonly IReachGraphRepository _repository; private readonly ReachGraphDigestComputer _digestComputer; private readonly ILogger _logger; public ReachGraphReplayService( IReachGraphStoreService storeService, IReachGraphRepository repository, ReachGraphDigestComputer digestComputer, ILogger logger) { _storeService = storeService ?? throw new ArgumentNullException(nameof(storeService)); _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _digestComputer = digestComputer ?? throw new ArgumentNullException(nameof(digestComputer)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task ReplayAsync( ReplayRequest request, string tenantId, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentException.ThrowIfNullOrEmpty(tenantId); var stopwatch = Stopwatch.StartNew(); try { // Get the original graph to compare var original = await _storeService.GetByDigestAsync( request.ExpectedDigest, tenantId, cancellationToken); if (original is null) { stopwatch.Stop(); return new ReplayResponse { Match = false, ComputedDigest = "N/A", ExpectedDigest = request.ExpectedDigest, DurationMs = (int)stopwatch.ElapsedMilliseconds, Divergence = new Models.ReplayDivergence { NodesAdded = 0, NodesRemoved = 0, EdgesChanged = 0 } }; } // Verify input digests match provenance var inputsVerified = VerifyInputs(request.Inputs, original.Provenance.Inputs); // Recompute digest from the stored graph (simulate replay) // In a full implementation, we would rebuild the graph from inputs var computedDigest = _digestComputer.ComputeDigest(original); stopwatch.Stop(); var match = string.Equals(computedDigest, request.ExpectedDigest, StringComparison.Ordinal); // Log the replay attempt await _repository.RecordReplayAsync(new ReplayLogEntry { SubgraphDigest = request.ExpectedDigest, InputDigests = original.Provenance.Inputs, ComputedDigest = computedDigest, Matches = match, TenantId = tenantId, DurationMs = (int)stopwatch.ElapsedMilliseconds }, cancellationToken); _logger.LogInformation( "Replay verification {Result}: expected={Expected}, computed={Computed}, duration={Duration}ms", match ? "MATCH" : "MISMATCH", request.ExpectedDigest, computedDigest, stopwatch.ElapsedMilliseconds); return new ReplayResponse { Match = match, ComputedDigest = computedDigest, ExpectedDigest = request.ExpectedDigest, DurationMs = (int)stopwatch.ElapsedMilliseconds, InputsVerified = inputsVerified }; } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, "Replay verification failed for digest {Digest}", request.ExpectedDigest); return new ReplayResponse { Match = false, ComputedDigest = "ERROR", ExpectedDigest = request.ExpectedDigest, DurationMs = (int)stopwatch.ElapsedMilliseconds }; } } private static ReplayInputsVerified VerifyInputs( ReplayInputs requested, Schema.ReachGraphInputs stored) { return new ReplayInputsVerified { Sbom = string.Equals(requested.Sbom, stored.Sbom, StringComparison.Ordinal), Vex = requested.Vex is not null && stored.Vex is not null ? string.Equals(requested.Vex, stored.Vex, StringComparison.Ordinal) : null, Callgraph = requested.Callgraph is not null && stored.Callgraph is not null ? string.Equals(requested.Callgraph, stored.Callgraph, StringComparison.Ordinal) : null, RuntimeFacts = requested.RuntimeFacts is not null && stored.RuntimeFacts is not null ? string.Equals(requested.RuntimeFacts, stored.RuntimeFacts, StringComparison.Ordinal) : null }; } }