139 lines
5.3 KiB
C#
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
|
|
};
|
|
}
|
|
}
|