Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
184
src/__Libraries/StellaOps.Replay/Engine/ReplayEngine.cs
Normal file
184
src/__Libraries/StellaOps.Replay/Engine/ReplayEngine.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Canonicalization.Json;
|
||||
using StellaOps.Canonicalization.Verification;
|
||||
using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
/// <summary>
|
||||
/// Executes scans deterministically from run manifests.
|
||||
/// Enables time-travel replay for verification and auditing.
|
||||
/// </summary>
|
||||
public sealed class ReplayEngine : IReplayEngine
|
||||
{
|
||||
private readonly IFeedLoader _feedLoader;
|
||||
private readonly IPolicyLoader _policyLoader;
|
||||
private readonly IScannerFactory _scannerFactory;
|
||||
private readonly ILogger<ReplayEngine> _logger;
|
||||
|
||||
public ReplayEngine(
|
||||
IFeedLoader feedLoader,
|
||||
IPolicyLoader policyLoader,
|
||||
IScannerFactory scannerFactory,
|
||||
ILogger<ReplayEngine> logger)
|
||||
{
|
||||
_feedLoader = feedLoader;
|
||||
_policyLoader = policyLoader;
|
||||
_scannerFactory = scannerFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ReplayResult> ReplayAsync(
|
||||
RunManifest manifest,
|
||||
ReplayOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogInformation("Starting replay for run {RunId}", manifest.RunId);
|
||||
|
||||
var validationResult = ValidateManifest(manifest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
return ReplayResult.Failed(manifest.RunId, "Manifest validation failed", validationResult.Errors);
|
||||
}
|
||||
|
||||
var feedResult = await LoadFeedSnapshotAsync(manifest.FeedSnapshot, ct).ConfigureAwait(false);
|
||||
if (!feedResult.Success)
|
||||
return ReplayResult.Failed(manifest.RunId, "Failed to load feed snapshot", [feedResult.Error ?? "Unknown error"]);
|
||||
|
||||
var policyResult = await LoadPolicySnapshotAsync(manifest.PolicySnapshot, ct).ConfigureAwait(false);
|
||||
if (!policyResult.Success)
|
||||
return ReplayResult.Failed(manifest.RunId, "Failed to load policy snapshot", [policyResult.Error ?? "Unknown error"]);
|
||||
|
||||
var scannerOptions = new ScannerOptions
|
||||
{
|
||||
FeedSnapshot = feedResult.Value!,
|
||||
PolicySnapshot = policyResult.Value!,
|
||||
CryptoProfile = manifest.CryptoProfile,
|
||||
PrngSeed = manifest.PrngSeed,
|
||||
FrozenTime = options.UseFrozenTime ? manifest.InitiatedAt : null,
|
||||
CanonicalizationVersion = manifest.CanonicalizationVersion
|
||||
};
|
||||
|
||||
var scanner = _scannerFactory.Create(scannerOptions);
|
||||
var scanResult = await scanner.ScanAsync(manifest.ArtifactDigests, ct).ConfigureAwait(false);
|
||||
|
||||
var (verdictJson, verdictDigest) = CanonicalJsonSerializer.SerializeWithDigest(scanResult.Verdict);
|
||||
|
||||
return new ReplayResult
|
||||
{
|
||||
RunId = manifest.RunId,
|
||||
Success = true,
|
||||
VerdictJson = verdictJson,
|
||||
VerdictDigest = verdictDigest,
|
||||
EvidenceIndex = scanResult.EvidenceIndex,
|
||||
ExecutedAt = DateTimeOffset.UtcNow,
|
||||
DurationMs = scanResult.DurationMs
|
||||
};
|
||||
}
|
||||
|
||||
public DeterminismCheckResult CheckDeterminism(ReplayResult a, ReplayResult b)
|
||||
{
|
||||
if (a.VerdictDigest == b.VerdictDigest)
|
||||
{
|
||||
return new DeterminismCheckResult
|
||||
{
|
||||
IsDeterministic = true,
|
||||
DigestA = a.VerdictDigest,
|
||||
DigestB = b.VerdictDigest,
|
||||
Differences = []
|
||||
};
|
||||
}
|
||||
|
||||
var differences = FindJsonDifferences(a.VerdictJson, b.VerdictJson);
|
||||
return new DeterminismCheckResult
|
||||
{
|
||||
IsDeterministic = false,
|
||||
DigestA = a.VerdictDigest,
|
||||
DigestB = b.VerdictDigest,
|
||||
Differences = differences
|
||||
};
|
||||
}
|
||||
|
||||
private static ValidationResult ValidateManifest(RunManifest manifest)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest.RunId))
|
||||
errors.Add("RunId is required");
|
||||
|
||||
if (manifest.ArtifactDigests.Length == 0)
|
||||
errors.Add("At least one artifact digest required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest.FeedSnapshot.Digest))
|
||||
errors.Add("Feed snapshot digest required");
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private async Task<LoadResult<FeedSnapshot>> LoadFeedSnapshotAsync(
|
||||
FeedSnapshot snapshot, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var feed = await _feedLoader.LoadByDigestAsync(snapshot.Digest, ct).ConfigureAwait(false);
|
||||
if (!string.Equals(feed.Digest, snapshot.Digest, StringComparison.OrdinalIgnoreCase))
|
||||
return LoadResult<FeedSnapshot>.Fail($"Feed digest mismatch: expected {snapshot.Digest}");
|
||||
return LoadResult<FeedSnapshot>.Ok(feed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoadResult<FeedSnapshot>.Fail($"Failed to load feed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LoadResult<PolicySnapshot>> LoadPolicySnapshotAsync(
|
||||
PolicySnapshot snapshot, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var policy = await _policyLoader.LoadByDigestAsync(snapshot.LatticeRulesDigest, ct).ConfigureAwait(false);
|
||||
return LoadResult<PolicySnapshot>.Ok(policy);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoadResult<PolicySnapshot>.Fail($"Failed to load policy: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<JsonDifference> FindJsonDifferences(string? a, string? b)
|
||||
{
|
||||
if (a is null || b is null)
|
||||
return [new JsonDifference("$", "One or both values are null")];
|
||||
|
||||
var verifier = new DeterminismVerifier();
|
||||
var result = verifier.Compare(a, b);
|
||||
return result.Differences.Select(d => new JsonDifference(d, "Value mismatch")).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IReplayEngine
|
||||
{
|
||||
Task<ReplayResult> ReplayAsync(RunManifest manifest, ReplayOptions options, CancellationToken ct = default);
|
||||
DeterminismCheckResult CheckDeterminism(ReplayResult a, ReplayResult b);
|
||||
}
|
||||
|
||||
public interface IScannerFactory
|
||||
{
|
||||
IScanner Create(ScannerOptions options);
|
||||
}
|
||||
|
||||
public interface IScanner
|
||||
{
|
||||
Task<ScanResult> ScanAsync(ImmutableArray<ArtifactDigest> artifacts, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IFeedLoader
|
||||
{
|
||||
Task<FeedSnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IPolicyLoader
|
||||
{
|
||||
Task<PolicySnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Canonicalization.Json;
|
||||
using StellaOps.Replay.Engine;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public sealed class FeedSnapshotLoader : IFeedLoader
|
||||
{
|
||||
private readonly IFeedStorage _storage;
|
||||
private readonly ILogger<FeedSnapshotLoader> _logger;
|
||||
|
||||
public FeedSnapshotLoader(IFeedStorage storage, ILogger<FeedSnapshotLoader> logger)
|
||||
{
|
||||
_storage = storage;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<FeedSnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Loading feed snapshot with digest {Digest}", digest);
|
||||
|
||||
var localPath = GetLocalPath(digest);
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var feed = await LoadFromFileAsync(localPath, ct).ConfigureAwait(false);
|
||||
VerifyDigest(feed, digest);
|
||||
return feed;
|
||||
}
|
||||
|
||||
var storedFeed = await _storage.GetByDigestAsync(digest, ct).ConfigureAwait(false);
|
||||
if (storedFeed is not null)
|
||||
{
|
||||
VerifyDigest(storedFeed, digest);
|
||||
return storedFeed;
|
||||
}
|
||||
|
||||
throw new FeedNotFoundException($"Feed snapshot not found: {digest}");
|
||||
}
|
||||
|
||||
private static void VerifyDigest(FeedSnapshot feed, string expected)
|
||||
{
|
||||
var actual = ComputeDigest(feed);
|
||||
if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new DigestMismatchException($"Feed digest mismatch: expected {expected}, got {actual}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeDigest(FeedSnapshot feed)
|
||||
{
|
||||
var json = CanonicalJsonSerializer.Serialize(feed);
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string GetLocalPath(string digest) =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"stellaops", "feeds", digest[..2], digest);
|
||||
|
||||
private static async Task<FeedSnapshot> LoadFromFileAsync(string path, CancellationToken ct)
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false);
|
||||
return CanonicalJsonSerializer.Deserialize<FeedSnapshot>(json);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IFeedStorage
|
||||
{
|
||||
Task<FeedSnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class FeedNotFoundException : Exception
|
||||
{
|
||||
public FeedNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
public sealed class DigestMismatchException : Exception
|
||||
{
|
||||
public DigestMismatchException(string message) : base(message) { }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Canonicalization.Json;
|
||||
using StellaOps.Replay.Engine;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public sealed class PolicySnapshotLoader : IPolicyLoader
|
||||
{
|
||||
private readonly IPolicyStorage _storage;
|
||||
private readonly ILogger<PolicySnapshotLoader> _logger;
|
||||
|
||||
public PolicySnapshotLoader(IPolicyStorage storage, ILogger<PolicySnapshotLoader> logger)
|
||||
{
|
||||
_storage = storage;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PolicySnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Loading policy snapshot with digest {Digest}", digest);
|
||||
|
||||
var localPath = GetLocalPath(digest);
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var policy = await LoadFromFileAsync(localPath, ct).ConfigureAwait(false);
|
||||
VerifyDigest(policy, digest);
|
||||
return policy;
|
||||
}
|
||||
|
||||
var stored = await _storage.GetByDigestAsync(digest, ct).ConfigureAwait(false);
|
||||
if (stored is not null)
|
||||
{
|
||||
VerifyDigest(stored, digest);
|
||||
return stored;
|
||||
}
|
||||
|
||||
throw new PolicyNotFoundException($"Policy snapshot not found: {digest}");
|
||||
}
|
||||
|
||||
private static void VerifyDigest(PolicySnapshot policy, string expected)
|
||||
{
|
||||
var actual = ComputeDigest(policy);
|
||||
if (!string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new DigestMismatchException($"Policy digest mismatch: expected {expected}, got {actual}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeDigest(PolicySnapshot policy)
|
||||
{
|
||||
var json = CanonicalJsonSerializer.Serialize(policy);
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string GetLocalPath(string digest) =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"stellaops", "policies", digest[..2], digest);
|
||||
|
||||
private static async Task<PolicySnapshot> LoadFromFileAsync(string path, CancellationToken ct)
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false);
|
||||
return CanonicalJsonSerializer.Deserialize<PolicySnapshot>(json);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IPolicyStorage
|
||||
{
|
||||
Task<PolicySnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class PolicyNotFoundException : Exception
|
||||
{
|
||||
public PolicyNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
54
src/__Libraries/StellaOps.Replay/Models/ReplayModels.cs
Normal file
54
src/__Libraries/StellaOps.Replay/Models/ReplayModels.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Evidence.Models;
|
||||
|
||||
namespace StellaOps.Replay.Models;
|
||||
|
||||
public sealed record ReplayResult
|
||||
{
|
||||
public required string RunId { get; init; }
|
||||
public bool Success { get; init; }
|
||||
public string? VerdictJson { get; init; }
|
||||
public string? VerdictDigest { get; init; }
|
||||
public EvidenceIndex? EvidenceIndex { get; init; }
|
||||
public DateTimeOffset ExecutedAt { get; init; }
|
||||
public long DurationMs { get; init; }
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
|
||||
public static ReplayResult Failed(string runId, string message, IReadOnlyList<string> errors) =>
|
||||
new()
|
||||
{
|
||||
RunId = runId,
|
||||
Success = false,
|
||||
Errors = errors.Prepend(message).ToList(),
|
||||
ExecutedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record DeterminismCheckResult
|
||||
{
|
||||
public bool IsDeterministic { get; init; }
|
||||
public string? DigestA { get; init; }
|
||||
public string? DigestB { get; init; }
|
||||
public IReadOnlyList<JsonDifference> Differences { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record JsonDifference(string Path, string Description);
|
||||
|
||||
public sealed record ReplayOptions
|
||||
{
|
||||
public bool UseFrozenTime { get; init; } = true;
|
||||
public bool VerifyDigests { get; init; } = true;
|
||||
public bool CaptureEvidence { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed record ValidationResult(bool IsValid, IReadOnlyList<string> Errors);
|
||||
|
||||
public sealed record LoadResult<T>
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public T? Value { get; init; }
|
||||
public string? Error { get; init; }
|
||||
|
||||
public static LoadResult<T> Ok(T value) => new() { Success = true, Value = value };
|
||||
public static LoadResult<T> Fail(string error) => new() { Success = false, Error = error };
|
||||
}
|
||||
19
src/__Libraries/StellaOps.Replay/Models/ScanModels.cs
Normal file
19
src/__Libraries/StellaOps.Replay/Models/ScanModels.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using StellaOps.Evidence.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Models;
|
||||
|
||||
public sealed record ScanResult(
|
||||
object Verdict,
|
||||
EvidenceIndex? EvidenceIndex,
|
||||
long DurationMs);
|
||||
|
||||
public sealed record ScannerOptions
|
||||
{
|
||||
public required FeedSnapshot FeedSnapshot { get; init; }
|
||||
public required PolicySnapshot PolicySnapshot { get; init; }
|
||||
public required CryptoProfile CryptoProfile { get; init; }
|
||||
public long? PrngSeed { get; init; }
|
||||
public DateTimeOffset? FrozenTime { get; init; }
|
||||
public required string CanonicalizationVersion { get; init; }
|
||||
}
|
||||
19
src/__Libraries/StellaOps.Replay/StellaOps.Replay.csproj
Normal file
19
src/__Libraries/StellaOps.Replay/StellaOps.Replay.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Canonicalization\StellaOps.Canonicalization.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Testing.Manifests\StellaOps.Testing.Manifests.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Evidence\StellaOps.Evidence.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user