Files
git.stella-ops.org/src/ReachGraph/StellaOps.ReachGraph.WebService/Services/ReachGraphReplayService.cs

139 lines
5.3 KiB
C#

// 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;
/// <summary>
/// Service for verifying deterministic replay of reachability graphs.
/// </summary>
public sealed class ReachGraphReplayService : IReachGraphReplayService
{
private readonly IReachGraphStoreService _storeService;
private readonly IReachGraphRepository _repository;
private readonly ReachGraphDigestComputer _digestComputer;
private readonly ILogger<ReachGraphReplayService> _logger;
public ReachGraphReplayService(
IReachGraphStoreService storeService,
IReachGraphRepository repository,
ReachGraphDigestComputer digestComputer,
ILogger<ReachGraphReplayService> 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<ReplayResponse> 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
};
}
}