more audit work

This commit is contained in:
master
2026-01-08 10:21:51 +02:00
parent 43c02081ef
commit 51cf4bc16c
546 changed files with 36721 additions and 4003 deletions

View File

@@ -0,0 +1,465 @@
// <copyright file="DeterminismVerifier.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace StellaOps.Replay.Core;
/// <summary>
/// Verifies determinism by comparing original and replay verdicts.
/// Sprint: SPRINT_20260107_006_005 Task RB-004
/// </summary>
public sealed class DeterminismVerifier
{
private readonly ILogger<DeterminismVerifier> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeterminismVerifier"/> class.
/// </summary>
public DeterminismVerifier(ILogger<DeterminismVerifier> logger)
{
_logger = logger;
}
/// <summary>
/// Verifies that the replay result matches the original verdict.
/// </summary>
/// <param name="original">The original verdict.</param>
/// <param name="replay">The replayed verdict.</param>
/// <returns>The verification result.</returns>
public VerificationResult Verify(VerdictRecord original, VerdictRecord replay)
{
var differences = new List<VerdictDifference>();
// Compare verdict digests
var originalDigest = ComputeVerdictDigest(original);
var replayDigest = ComputeVerdictDigest(replay);
var digestsMatch = string.Equals(originalDigest, replayDigest, StringComparison.Ordinal);
if (!digestsMatch)
{
// Identify specific differences
differences.AddRange(FindDifferences(original, replay));
}
var result = new VerificationResult
{
OriginalDigest = originalDigest,
ReplayDigest = replayDigest,
IsDeterministic = digestsMatch,
Differences = differences.ToImmutableArray(),
VerifiedAt = DateTimeOffset.UtcNow,
DeterminismScore = CalculateDeterminismScore(original, replay, differences)
};
if (digestsMatch)
{
_logger.LogInformation(
"Determinism verified: original and replay match (digest: {Digest})",
originalDigest);
}
else
{
_logger.LogWarning(
"Determinism violation: {Count} differences found between original ({Original}) and replay ({Replay})",
differences.Count, originalDigest, replayDigest);
}
return result;
}
/// <summary>
/// Computes a canonical digest of a verdict for comparison.
/// </summary>
public string ComputeVerdictDigest(VerdictRecord verdict)
{
// Create canonical representation for hashing
var canonical = new
{
verdict.Outcome,
verdict.Severity,
verdict.PolicyId,
verdict.RuleIds,
FindingCount = verdict.Findings.Length,
FindingDigests = verdict.Findings
.OrderBy(f => f.FindingId, StringComparer.Ordinal)
.Select(f => ComputeFindingDigest(f))
.ToArray()
};
var json = JsonSerializer.Serialize(canonical, CanonicalJsonOptions);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Generates a diff report for non-matching verdicts.
/// </summary>
public string GenerateDiffReport(VerificationResult result)
{
var sb = new StringBuilder();
sb.AppendLine("# Determinism Verification Report");
sb.AppendLine();
sb.AppendLine(string.Format(CultureInfo.InvariantCulture,
"**Verified At:** {0:O}", result.VerifiedAt));
sb.AppendLine(string.Format(CultureInfo.InvariantCulture,
"**Determinism Score:** {0:P1}", result.DeterminismScore));
sb.AppendLine();
sb.AppendLine("## Digests");
sb.AppendLine();
sb.AppendLine($"| Version | Digest |");
sb.AppendLine($"|---------|--------|");
sb.AppendLine($"| Original | `{result.OriginalDigest}` |");
sb.AppendLine($"| Replay | `{result.ReplayDigest}` |");
sb.AppendLine();
if (result.IsDeterministic)
{
sb.AppendLine("## Result: MATCH");
sb.AppendLine();
sb.AppendLine("The replay produced an identical verdict to the original.");
}
else
{
sb.AppendLine("## Result: MISMATCH");
sb.AppendLine();
sb.AppendLine($"Found **{result.Differences.Length}** difference(s):");
sb.AppendLine();
foreach (var diff in result.Differences)
{
sb.AppendLine($"### {diff.Field}");
sb.AppendLine();
sb.AppendLine($"- **Original:** `{diff.OriginalValue}`");
sb.AppendLine($"- **Replay:** `{diff.ReplayValue}`");
sb.AppendLine($"- **Severity:** {diff.Severity}");
if (!string.IsNullOrEmpty(diff.Explanation))
{
sb.AppendLine($"- **Explanation:** {diff.Explanation}");
}
sb.AppendLine();
}
sb.AppendLine("## Possible Causes");
sb.AppendLine();
sb.AppendLine("- Non-deterministic timestamp usage");
sb.AppendLine("- Random number generation without fixed seed");
sb.AppendLine("- Unstable ordering in collections");
sb.AppendLine("- External service dependencies");
sb.AppendLine("- Feed data drift between original and replay");
}
return sb.ToString();
}
private static string ComputeFindingDigest(FindingRecord finding)
{
var canonical = $"{finding.FindingId}:{finding.VulnerabilityId}:{finding.Component}:{finding.Severity}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical));
return Convert.ToHexString(hash).ToLowerInvariant()[..16];
}
private static List<VerdictDifference> FindDifferences(VerdictRecord original, VerdictRecord replay)
{
var differences = new List<VerdictDifference>();
// Check outcome
if (original.Outcome != replay.Outcome)
{
differences.Add(new VerdictDifference
{
Field = "Outcome",
OriginalValue = original.Outcome.ToString(),
ReplayValue = replay.Outcome.ToString(),
Severity = DifferenceSeverity.Critical,
Explanation = "The final pass/fail decision differs"
});
}
// Check severity
if (original.Severity != replay.Severity)
{
differences.Add(new VerdictDifference
{
Field = "Severity",
OriginalValue = original.Severity,
ReplayValue = replay.Severity,
Severity = DifferenceSeverity.High,
Explanation = "The severity assessment differs"
});
}
// Check finding count
if (original.Findings.Length != replay.Findings.Length)
{
differences.Add(new VerdictDifference
{
Field = "FindingCount",
OriginalValue = original.Findings.Length.ToString(CultureInfo.InvariantCulture),
ReplayValue = replay.Findings.Length.ToString(CultureInfo.InvariantCulture),
Severity = DifferenceSeverity.High,
Explanation = "Different number of findings detected"
});
}
// Check individual findings
var originalFindings = original.Findings.ToDictionary(f => f.FindingId);
var replayFindings = replay.Findings.ToDictionary(f => f.FindingId);
var missingInReplay = originalFindings.Keys.Except(replayFindings.Keys).ToList();
var newInReplay = replayFindings.Keys.Except(originalFindings.Keys).ToList();
foreach (var id in missingInReplay)
{
differences.Add(new VerdictDifference
{
Field = $"Finding:{id}",
OriginalValue = "Present",
ReplayValue = "Missing",
Severity = DifferenceSeverity.Medium,
Explanation = $"Finding {id} was not reproduced"
});
}
foreach (var id in newInReplay)
{
differences.Add(new VerdictDifference
{
Field = $"Finding:{id}",
OriginalValue = "Missing",
ReplayValue = "Present",
Severity = DifferenceSeverity.Medium,
Explanation = $"Finding {id} is new in replay"
});
}
// Check common findings for property differences
foreach (var id in originalFindings.Keys.Intersect(replayFindings.Keys))
{
var origFinding = originalFindings[id];
var replayFinding = replayFindings[id];
if (origFinding.Severity != replayFinding.Severity)
{
differences.Add(new VerdictDifference
{
Field = $"Finding:{id}:Severity",
OriginalValue = origFinding.Severity,
ReplayValue = replayFinding.Severity,
Severity = DifferenceSeverity.Medium,
Explanation = "Finding severity differs"
});
}
}
// Check rule evaluation order
if (!original.RuleIds.SequenceEqual(replay.RuleIds))
{
differences.Add(new VerdictDifference
{
Field = "RuleOrder",
OriginalValue = string.Join(",", original.RuleIds),
ReplayValue = string.Join(",", replay.RuleIds),
Severity = DifferenceSeverity.Low,
Explanation = "Rule evaluation order differs"
});
}
return differences;
}
private static double CalculateDeterminismScore(
VerdictRecord original,
VerdictRecord replay,
List<VerdictDifference> differences)
{
if (differences.Count == 0)
{
return 1.0;
}
// Weight differences by severity
double penalty = 0;
foreach (var diff in differences)
{
penalty += diff.Severity switch
{
DifferenceSeverity.Critical => 0.5,
DifferenceSeverity.High => 0.2,
DifferenceSeverity.Medium => 0.1,
DifferenceSeverity.Low => 0.05,
_ => 0.05
};
}
return Math.Max(0, 1.0 - penalty);
}
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
PropertyNamingPolicy = null,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
}
/// <summary>
/// Result of determinism verification.
/// </summary>
public sealed record VerificationResult
{
/// <summary>
/// Gets the original verdict digest.
/// </summary>
public required string OriginalDigest { get; init; }
/// <summary>
/// Gets the replay verdict digest.
/// </summary>
public required string ReplayDigest { get; init; }
/// <summary>
/// Gets whether the verdicts are deterministic (match).
/// </summary>
public bool IsDeterministic { get; init; }
/// <summary>
/// Gets the differences found.
/// </summary>
public ImmutableArray<VerdictDifference> Differences { get; init; } =
ImmutableArray<VerdictDifference>.Empty;
/// <summary>
/// Gets when verification was performed.
/// </summary>
public DateTimeOffset VerifiedAt { get; init; }
/// <summary>
/// Gets the determinism score (0-1, 1 = perfect match).
/// </summary>
public double DeterminismScore { get; init; }
}
/// <summary>
/// A difference between original and replay verdicts.
/// </summary>
public sealed record VerdictDifference
{
/// <summary>
/// Gets the field that differs.
/// </summary>
public required string Field { get; init; }
/// <summary>
/// Gets the original value.
/// </summary>
public required string OriginalValue { get; init; }
/// <summary>
/// Gets the replay value.
/// </summary>
public required string ReplayValue { get; init; }
/// <summary>
/// Gets the severity of the difference.
/// </summary>
public required DifferenceSeverity Severity { get; init; }
/// <summary>
/// Gets an explanation of the difference.
/// </summary>
public string? Explanation { get; init; }
}
/// <summary>
/// Severity of a verdict difference.
/// </summary>
public enum DifferenceSeverity
{
/// <summary>Low severity (cosmetic).</summary>
Low,
/// <summary>Medium severity (detail differences).</summary>
Medium,
/// <summary>High severity (significant differences).</summary>
High,
/// <summary>Critical severity (outcome differs).</summary>
Critical
}
// Verdict models (would reference actual modules)
/// <summary>
/// A verdict record from policy evaluation.
/// </summary>
public sealed record VerdictRecord
{
/// <summary>Gets the verdict ID.</summary>
public required string VerdictId { get; init; }
/// <summary>Gets the outcome.</summary>
public required VerdictOutcome Outcome { get; init; }
/// <summary>Gets the severity.</summary>
public required string Severity { get; init; }
/// <summary>Gets the policy ID used.</summary>
public required string PolicyId { get; init; }
/// <summary>Gets the rule IDs evaluated.</summary>
public ImmutableArray<string> RuleIds { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Gets the findings.</summary>
public ImmutableArray<FindingRecord> Findings { get; init; } = ImmutableArray<FindingRecord>.Empty;
/// <summary>Gets when the verdict was rendered.</summary>
public DateTimeOffset RenderedAt { get; init; }
}
/// <summary>
/// Verdict outcome.
/// </summary>
public enum VerdictOutcome
{
/// <summary>Policy passed.</summary>
Pass,
/// <summary>Policy failed.</summary>
Fail,
/// <summary>Warning (pass with issues).</summary>
Warn,
/// <summary>Error during evaluation.</summary>
Error
}
/// <summary>
/// A finding record.
/// </summary>
public sealed record FindingRecord
{
/// <summary>Gets the finding ID.</summary>
public required string FindingId { get; init; }
/// <summary>Gets the vulnerability ID.</summary>
public required string VulnerabilityId { get; init; }
/// <summary>Gets the component.</summary>
public required string Component { get; init; }
/// <summary>Gets the severity.</summary>
public required string Severity { get; init; }
}

View File

@@ -0,0 +1,492 @@
// <copyright file="InputManifestResolver.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace StellaOps.Replay.Core;
/// <summary>
/// Resolves input manifests for deterministic replay.
/// Sprint: SPRINT_20260107_006_005 Task RB-002
/// </summary>
public sealed class InputManifestResolver
{
private readonly IFeedSnapshotStore _feedStore;
private readonly IPolicyManifestStore _policyStore;
private readonly IVexDocumentStore _vexStore;
private readonly IMemoryCache _cache;
private readonly ILogger<InputManifestResolver> _logger;
private readonly InputManifestResolverOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="InputManifestResolver"/> class.
/// </summary>
public InputManifestResolver(
IFeedSnapshotStore feedStore,
IPolicyManifestStore policyStore,
IVexDocumentStore vexStore,
IMemoryCache cache,
ILogger<InputManifestResolver> logger,
InputManifestResolverOptions? options = null)
{
_feedStore = feedStore;
_policyStore = policyStore;
_vexStore = vexStore;
_cache = cache;
_logger = logger;
_options = options ?? new InputManifestResolverOptions();
}
/// <summary>
/// Resolves all inputs required for replay.
/// </summary>
/// <param name="manifest">The input manifest specifying what to resolve.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The resolved inputs or errors.</returns>
public async Task<ResolvedInputs> ResolveAsync(
InputManifest manifest,
CancellationToken cancellationToken = default)
{
var errors = new List<InputResolutionError>();
// Resolve feed snapshot
var feedData = await ResolveFeedSnapshotAsync(
manifest.FeedSnapshotHash, errors, cancellationToken).ConfigureAwait(false);
// Resolve policy manifest
var policyBundle = await ResolvePolicyManifestAsync(
manifest.PolicyManifestHash, errors, cancellationToken).ConfigureAwait(false);
// Resolve VEX documents
var vexDocuments = await ResolveVexDocumentsAsync(
manifest.VexDocumentHashes, errors, cancellationToken).ConfigureAwait(false);
// Build resolved inputs
return new ResolvedInputs
{
FeedData = feedData,
PolicyBundle = policyBundle,
VexDocuments = vexDocuments,
SourceCodeHash = manifest.SourceCodeHash,
BaseImageDigest = manifest.BaseImageDigest,
ToolchainVersion = manifest.ToolchainVersion,
RandomSeed = manifest.RandomSeed,
TimestampOverride = manifest.TimestampOverride,
Errors = errors.ToImmutableArray(),
IsComplete = errors.Count == 0
};
}
/// <summary>
/// Validates that a manifest can be resolved without actually fetching data.
/// </summary>
public async Task<ManifestValidationResult> ValidateAsync(
InputManifest manifest,
CancellationToken cancellationToken = default)
{
var missing = new List<string>();
// Check feed snapshot exists
if (!string.IsNullOrEmpty(manifest.FeedSnapshotHash))
{
var feedExists = await _feedStore.ExistsAsync(manifest.FeedSnapshotHash, cancellationToken)
.ConfigureAwait(false);
if (!feedExists)
{
missing.Add($"Feed snapshot: {manifest.FeedSnapshotHash}");
}
}
// Check policy manifest exists
if (!string.IsNullOrEmpty(manifest.PolicyManifestHash))
{
var policyExists = await _policyStore.ExistsAsync(manifest.PolicyManifestHash, cancellationToken)
.ConfigureAwait(false);
if (!policyExists)
{
missing.Add($"Policy manifest: {manifest.PolicyManifestHash}");
}
}
// Check VEX documents exist
foreach (var vexHash in manifest.VexDocumentHashes)
{
var vexExists = await _vexStore.ExistsAsync(vexHash, cancellationToken)
.ConfigureAwait(false);
if (!vexExists)
{
missing.Add($"VEX document: {vexHash}");
}
}
return new ManifestValidationResult
{
IsValid = missing.Count == 0,
MissingInputs = missing.ToImmutableArray()
};
}
private async Task<FeedData?> ResolveFeedSnapshotAsync(
string? hash,
List<InputResolutionError> errors,
CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(hash))
{
return null;
}
var cacheKey = $"feed:{hash}";
if (_cache.TryGetValue(cacheKey, out FeedData? cached) && cached is not null)
{
_logger.LogDebug("Feed snapshot {Hash} resolved from cache", hash);
return cached;
}
try
{
var data = await _feedStore.GetAsync(hash, cancellationToken).ConfigureAwait(false);
if (data is null)
{
errors.Add(new InputResolutionError(InputType.FeedSnapshot, hash, "Not found"));
return null;
}
_cache.Set(cacheKey, data, _options.CacheDuration);
_logger.LogDebug("Feed snapshot {Hash} resolved and cached", hash);
return data;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to resolve feed snapshot {Hash}", hash);
errors.Add(new InputResolutionError(InputType.FeedSnapshot, hash, ex.Message));
return null;
}
}
private async Task<PolicyBundle?> ResolvePolicyManifestAsync(
string? hash,
List<InputResolutionError> errors,
CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(hash))
{
return null;
}
var cacheKey = $"policy:{hash}";
if (_cache.TryGetValue(cacheKey, out PolicyBundle? cached) && cached is not null)
{
_logger.LogDebug("Policy manifest {Hash} resolved from cache", hash);
return cached;
}
try
{
var bundle = await _policyStore.GetAsync(hash, cancellationToken).ConfigureAwait(false);
if (bundle is null)
{
errors.Add(new InputResolutionError(InputType.PolicyManifest, hash, "Not found"));
return null;
}
_cache.Set(cacheKey, bundle, _options.CacheDuration);
_logger.LogDebug("Policy manifest {Hash} resolved and cached", hash);
return bundle;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to resolve policy manifest {Hash}", hash);
errors.Add(new InputResolutionError(InputType.PolicyManifest, hash, ex.Message));
return null;
}
}
private async Task<ImmutableArray<VexDocument>> ResolveVexDocumentsAsync(
ImmutableArray<string> hashes,
List<InputResolutionError> errors,
CancellationToken cancellationToken)
{
if (hashes.IsEmpty)
{
return ImmutableArray<VexDocument>.Empty;
}
var documents = new List<VexDocument>();
foreach (var hash in hashes)
{
var cacheKey = $"vex:{hash}";
if (_cache.TryGetValue(cacheKey, out VexDocument? cached) && cached is not null)
{
documents.Add(cached);
continue;
}
try
{
var doc = await _vexStore.GetAsync(hash, cancellationToken).ConfigureAwait(false);
if (doc is null)
{
errors.Add(new InputResolutionError(InputType.VexDocument, hash, "Not found"));
continue;
}
_cache.Set(cacheKey, doc, _options.CacheDuration);
documents.Add(doc);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to resolve VEX document {Hash}", hash);
errors.Add(new InputResolutionError(InputType.VexDocument, hash, ex.Message));
}
}
return documents.ToImmutableArray();
}
}
/// <summary>
/// Input manifest specifying what to resolve for replay.
/// </summary>
public sealed record InputManifest
{
/// <summary>
/// Gets the feed snapshot hash.
/// </summary>
public string? FeedSnapshotHash { get; init; }
/// <summary>
/// Gets the policy manifest hash.
/// </summary>
public string? PolicyManifestHash { get; init; }
/// <summary>
/// Gets the source code hash.
/// </summary>
public string? SourceCodeHash { get; init; }
/// <summary>
/// Gets the base image digest.
/// </summary>
public string? BaseImageDigest { get; init; }
/// <summary>
/// Gets the VEX document hashes.
/// </summary>
public ImmutableArray<string> VexDocumentHashes { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Gets the toolchain version used.
/// </summary>
public string? ToolchainVersion { get; init; }
/// <summary>
/// Gets the random seed for determinism.
/// </summary>
public int? RandomSeed { get; init; }
/// <summary>
/// Gets the timestamp override for determinism.
/// </summary>
public DateTimeOffset? TimestampOverride { get; init; }
}
/// <summary>
/// Resolved inputs ready for replay.
/// </summary>
public sealed record ResolvedInputs
{
/// <summary>
/// Gets the resolved feed data.
/// </summary>
public FeedData? FeedData { get; init; }
/// <summary>
/// Gets the resolved policy bundle.
/// </summary>
public PolicyBundle? PolicyBundle { get; init; }
/// <summary>
/// Gets the resolved VEX documents.
/// </summary>
public ImmutableArray<VexDocument> VexDocuments { get; init; } = ImmutableArray<VexDocument>.Empty;
/// <summary>
/// Gets the source code hash.
/// </summary>
public string? SourceCodeHash { get; init; }
/// <summary>
/// Gets the base image digest.
/// </summary>
public string? BaseImageDigest { get; init; }
/// <summary>
/// Gets the toolchain version.
/// </summary>
public string? ToolchainVersion { get; init; }
/// <summary>
/// Gets the random seed.
/// </summary>
public int? RandomSeed { get; init; }
/// <summary>
/// Gets the timestamp override.
/// </summary>
public DateTimeOffset? TimestampOverride { get; init; }
/// <summary>
/// Gets any resolution errors.
/// </summary>
public ImmutableArray<InputResolutionError> Errors { get; init; } =
ImmutableArray<InputResolutionError>.Empty;
/// <summary>
/// Gets whether all inputs were resolved successfully.
/// </summary>
public bool IsComplete { get; init; }
}
/// <summary>
/// Error resolving an input.
/// </summary>
public sealed record InputResolutionError(InputType Type, string Hash, string Message);
/// <summary>
/// Types of replay inputs.
/// </summary>
public enum InputType
{
/// <summary>Feed snapshot.</summary>
FeedSnapshot,
/// <summary>Policy manifest.</summary>
PolicyManifest,
/// <summary>VEX document.</summary>
VexDocument,
/// <summary>Source code.</summary>
SourceCode,
/// <summary>Base image.</summary>
BaseImage
}
/// <summary>
/// Result of manifest validation.
/// </summary>
public sealed record ManifestValidationResult
{
/// <summary>
/// Gets whether all inputs are available.
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Gets the missing inputs.
/// </summary>
public ImmutableArray<string> MissingInputs { get; init; } = ImmutableArray<string>.Empty;
}
/// <summary>
/// Options for input manifest resolution.
/// </summary>
public sealed class InputManifestResolverOptions
{
/// <summary>
/// Gets or sets the cache duration for resolved inputs.
/// Default: 1 hour.
/// </summary>
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromHours(1);
}
// Store interfaces (to be implemented elsewhere)
/// <summary>
/// Interface for feed snapshot storage.
/// </summary>
public interface IFeedSnapshotStore
{
/// <summary>Gets feed data by hash.</summary>
Task<FeedData?> GetAsync(string hash, CancellationToken cancellationToken);
/// <summary>Checks if feed data exists.</summary>
Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken);
}
/// <summary>
/// Interface for policy manifest storage.
/// </summary>
public interface IPolicyManifestStore
{
/// <summary>Gets policy bundle by hash.</summary>
Task<PolicyBundle?> GetAsync(string hash, CancellationToken cancellationToken);
/// <summary>Checks if policy bundle exists.</summary>
Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken);
}
/// <summary>
/// Interface for VEX document storage.
/// </summary>
public interface IVexDocumentStore
{
/// <summary>Gets VEX document by hash.</summary>
Task<VexDocument?> GetAsync(string hash, CancellationToken cancellationToken);
/// <summary>Checks if VEX document exists.</summary>
Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken);
}
// Data models (placeholders - would reference actual modules)
/// <summary>
/// Feed data snapshot.
/// </summary>
public sealed record FeedData
{
/// <summary>Gets the hash.</summary>
public required string Hash { get; init; }
/// <summary>Gets the content.</summary>
public required byte[] Content { get; init; }
/// <summary>Gets the snapshot timestamp.</summary>
public required DateTimeOffset SnapshotAt { get; init; }
}
/// <summary>
/// Policy bundle.
/// </summary>
public sealed record PolicyBundle
{
/// <summary>Gets the hash.</summary>
public required string Hash { get; init; }
/// <summary>Gets the policy rules.</summary>
public required ImmutableArray<byte> Content { get; init; }
/// <summary>Gets the bundle version.</summary>
public required string Version { get; init; }
}
/// <summary>
/// VEX document.
/// </summary>
public sealed record VexDocument
{
/// <summary>Gets the hash.</summary>
public required string Hash { get; init; }
/// <summary>Gets the content.</summary>
public required string Content { get; init; }
/// <summary>Gets the format (OpenVEX, CSAF, etc.).</summary>
public required string Format { get; init; }
}

View File

@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Description>Replay.Core - Deterministic replay and verification for StellaOps</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>