Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user