save work
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Policy.Scoring;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
public sealed class DeterministicScoringService : IScoringService
|
||||
{
|
||||
public Task<double> ReplayScoreAsync(
|
||||
string scanId,
|
||||
string concelierSnapshotHash,
|
||||
string excititorSnapshotHash,
|
||||
string latticePolicyHash,
|
||||
byte[] seed,
|
||||
DateTimeOffset freezeTimestamp,
|
||||
ProofLedger ledger,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scanId);
|
||||
ArgumentNullException.ThrowIfNull(seed);
|
||||
ArgumentNullException.ThrowIfNull(ledger);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var input = string.Join(
|
||||
"|",
|
||||
scanId.Trim(),
|
||||
concelierSnapshotHash?.Trim() ?? string.Empty,
|
||||
excititorSnapshotHash?.Trim() ?? string.Empty,
|
||||
latticePolicyHash?.Trim() ?? string.Empty,
|
||||
freezeTimestamp.ToUniversalTime().ToString("O"),
|
||||
Convert.ToHexStringLower(seed));
|
||||
|
||||
var digest = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
var value = BinaryPrimitives.ReadUInt64BigEndian(digest.AsSpan(0, sizeof(ulong)));
|
||||
var score = value / (double)ulong.MaxValue;
|
||||
score = Math.Clamp(score, 0.0, 1.0);
|
||||
|
||||
var actor = "scanner.webservice.score";
|
||||
var evidenceRefs = new[]
|
||||
{
|
||||
concelierSnapshotHash,
|
||||
excititorSnapshotHash,
|
||||
latticePolicyHash
|
||||
}.Where(v => !string.IsNullOrWhiteSpace(v)).ToArray();
|
||||
|
||||
var inputNodeId = $"input:{scanId}";
|
||||
ledger.Append(ProofNode.CreateInput(
|
||||
id: inputNodeId,
|
||||
ruleId: "deterministic",
|
||||
actor: actor,
|
||||
tsUtc: freezeTimestamp,
|
||||
seed: seed,
|
||||
initialValue: score,
|
||||
evidenceRefs: evidenceRefs));
|
||||
|
||||
ledger.Append(ProofNode.CreateScore(
|
||||
id: $"score:{scanId}",
|
||||
ruleId: "deterministic",
|
||||
actor: actor,
|
||||
tsUtc: freezeTimestamp,
|
||||
seed: seed,
|
||||
finalScore: score,
|
||||
parentIds: new[] { inputNodeId }));
|
||||
|
||||
return Task.FromResult(score);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Scanner.Core;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
public sealed class InMemoryProofBundleRepository : IProofBundleRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, ProofBundle>> _bundles
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public Task<ProofBundle?> GetBundleAsync(string scanId, string? rootHash = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scanId))
|
||||
{
|
||||
return Task.FromResult<ProofBundle?>(null);
|
||||
}
|
||||
|
||||
if (!_bundles.TryGetValue(scanId.Trim(), out var bundlesByRootHash) || bundlesByRootHash.Count == 0)
|
||||
{
|
||||
return Task.FromResult<ProofBundle?>(null);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rootHash))
|
||||
{
|
||||
var normalizedHash = NormalizeDigest(rootHash);
|
||||
return Task.FromResult(bundlesByRootHash.TryGetValue(normalizedHash, out var bundle) ? bundle : null);
|
||||
}
|
||||
|
||||
var best = bundlesByRootHash.Values
|
||||
.OrderByDescending(b => b.CreatedAtUtc)
|
||||
.ThenBy(b => b.RootHash, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
|
||||
return Task.FromResult(best);
|
||||
}
|
||||
|
||||
public Task SaveBundleAsync(ProofBundle bundle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bundle);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var bundlesByRootHash = _bundles.GetOrAdd(
|
||||
bundle.ScanId.Trim(),
|
||||
_ => new ConcurrentDictionary<string, ProofBundle>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
bundlesByRootHash[NormalizeDigest(bundle.RootHash)] = bundle;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string NormalizeDigest(string value)
|
||||
{
|
||||
var trimmed = value.Trim();
|
||||
if (!trimmed.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
trimmed = $"sha256:{trimmed}";
|
||||
}
|
||||
|
||||
return trimmed.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Core;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
public sealed class InMemoryScanManifestRepository : IScanManifestRepository
|
||||
{
|
||||
private readonly IScanCoordinator _scanCoordinator;
|
||||
private readonly IScanManifestSigner _manifestSigner;
|
||||
private readonly ILogger<InMemoryScanManifestRepository> _logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, SignedScanManifest>> _manifestsByScanId
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public InMemoryScanManifestRepository(
|
||||
IScanCoordinator scanCoordinator,
|
||||
IScanManifestSigner manifestSigner,
|
||||
ILogger<InMemoryScanManifestRepository> logger)
|
||||
{
|
||||
_scanCoordinator = scanCoordinator ?? throw new ArgumentNullException(nameof(scanCoordinator));
|
||||
_manifestSigner = manifestSigner ?? throw new ArgumentNullException(nameof(manifestSigner));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<SignedScanManifest?> GetManifestAsync(
|
||||
string scanId,
|
||||
string? manifestHash = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scanId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedScanId = scanId.Trim();
|
||||
var normalizedManifestHash = NormalizeDigest(manifestHash);
|
||||
|
||||
if (_manifestsByScanId.TryGetValue(normalizedScanId, out var existingByHash))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(normalizedManifestHash))
|
||||
{
|
||||
return existingByHash.TryGetValue(normalizedManifestHash, out var hit) ? hit : null;
|
||||
}
|
||||
|
||||
return SelectDefault(existingByHash);
|
||||
}
|
||||
|
||||
var snapshot = await _scanCoordinator.GetAsync(new ScanId(normalizedScanId), cancellationToken).ConfigureAwait(false);
|
||||
if (snapshot is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var manifest = BuildManifest(snapshot);
|
||||
var signed = await _manifestSigner.SignAsync(manifest, cancellationToken).ConfigureAwait(false);
|
||||
await SaveManifestAsync(signed, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(normalizedManifestHash)
|
||||
&& !string.Equals(NormalizeDigest(signed.ManifestHash), normalizedManifestHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Created scan manifest for scan {ScanId} ({ManifestHash})", normalizedScanId, signed.ManifestHash);
|
||||
return signed;
|
||||
}
|
||||
|
||||
public Task SaveManifestAsync(SignedScanManifest manifest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var scanId = manifest.Manifest.ScanId.Trim();
|
||||
var hash = NormalizeDigest(manifest.ManifestHash) ?? manifest.ManifestHash.Trim();
|
||||
|
||||
var byHash = _manifestsByScanId.GetOrAdd(scanId, _ => new ConcurrentDictionary<string, SignedScanManifest>(StringComparer.OrdinalIgnoreCase));
|
||||
byHash[hash] = manifest;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<List<string>> FindAffectedScansAsync(AffectedScansQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return Task.FromResult(new List<string>());
|
||||
}
|
||||
|
||||
private static SignedScanManifest? SelectDefault(ConcurrentDictionary<string, SignedScanManifest> manifestsByHash)
|
||||
{
|
||||
if (manifestsByHash.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return manifestsByHash.Values
|
||||
.OrderByDescending(m => m.Manifest.CreatedAtUtc)
|
||||
.ThenBy(m => m.ManifestHash, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static ScanManifest BuildManifest(ScanSnapshot snapshot)
|
||||
{
|
||||
var targetDigest = NormalizeDigest(snapshot.Target.Digest) ?? "sha256:unknown";
|
||||
var seed = SHA256.HashData(Encoding.UTF8.GetBytes(snapshot.ScanId.Value));
|
||||
|
||||
var version = typeof(InMemoryScanManifestRepository).Assembly.GetName().Version?.ToString() ?? "0.0.0";
|
||||
|
||||
return ScanManifest.CreateBuilder(snapshot.ScanId.Value, targetDigest)
|
||||
.WithCreatedAt(snapshot.CreatedAt)
|
||||
.WithScannerVersion(version)
|
||||
.WithWorkerVersion(version)
|
||||
.WithConcelierSnapshot(ComputeDigest($"concelier:{snapshot.ScanId.Value}"))
|
||||
.WithExcititorSnapshot(ComputeDigest($"excititor:{snapshot.ScanId.Value}"))
|
||||
.WithLatticePolicyHash(ComputeDigest($"policy:{snapshot.ScanId.Value}"))
|
||||
.WithDeterministic(true)
|
||||
.WithSeed(seed)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static string ComputeDigest(string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
private static string? NormalizeDigest(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (!trimmed.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
trimmed = $"sha256:{trimmed}";
|
||||
}
|
||||
|
||||
return trimmed.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -394,26 +394,59 @@ internal sealed class RuntimeInventoryReconciler : IRuntimeInventoryReconciler
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = _storageOptions.CurrentValue;
|
||||
var key = ArtifactObjectKeyBuilder.Build(
|
||||
|
||||
var primaryKey = ArtifactObjectKeyBuilder.Build(
|
||||
artifact.Type,
|
||||
artifact.Format,
|
||||
artifact.BytesSha256,
|
||||
options.ObjectStore.RootPrefix);
|
||||
|
||||
var descriptor = new ArtifactObjectDescriptor(
|
||||
options.ObjectStore.BucketName,
|
||||
key,
|
||||
artifact.Immutable);
|
||||
var candidates = new List<string>
|
||||
{
|
||||
primaryKey,
|
||||
ArtifactObjectKeyBuilder.Build(
|
||||
artifact.Type,
|
||||
artifact.Format,
|
||||
artifact.BytesSha256,
|
||||
rootPrefix: null)
|
||||
};
|
||||
|
||||
var legacyDigest = NormalizeLegacyDigest(artifact.BytesSha256);
|
||||
candidates.Add($"{MapLegacyTypeSegment(artifact.Type)}/{MapLegacyFormatSegment(artifact.Format)}/{legacyDigest}");
|
||||
|
||||
if (legacyDigest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
candidates.Add($"{MapLegacyTypeSegment(artifact.Type)}/{MapLegacyFormatSegment(artifact.Format)}/{legacyDigest["sha256:".Length..]}");
|
||||
}
|
||||
|
||||
Stream? stream = null;
|
||||
string? resolvedKey = null;
|
||||
|
||||
foreach (var candidateKey in candidates.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var descriptor = new ArtifactObjectDescriptor(
|
||||
options.ObjectStore.BucketName,
|
||||
candidateKey,
|
||||
artifact.Immutable);
|
||||
|
||||
stream = await _objectStore.GetAsync(descriptor, cancellationToken).ConfigureAwait(false);
|
||||
if (stream is not null)
|
||||
{
|
||||
resolvedKey = candidateKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await using var stream = await _objectStore.GetAsync(descriptor, cancellationToken).ConfigureAwait(false);
|
||||
if (stream is null)
|
||||
{
|
||||
_logger.LogWarning("SBOM artifact content not found at {Key}", key);
|
||||
_logger.LogWarning("SBOM artifact content not found at {Key}", primaryKey);
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using (stream)
|
||||
{
|
||||
var bom = await Serializer.DeserializeAsync(stream).ConfigureAwait(false);
|
||||
if (bom?.Components is null)
|
||||
{
|
||||
@@ -435,10 +468,11 @@ internal sealed class RuntimeInventoryReconciler : IRuntimeInventoryReconciler
|
||||
FilePaths = ExtractFilePaths(c)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to deserialize SBOM from artifact {ArtifactId}", artifact.Id);
|
||||
_logger.LogWarning(ex, "Failed to deserialize SBOM from artifact {ArtifactId} ({ResolvedKey})", artifact.Id, resolvedKey ?? primaryKey);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -595,6 +629,38 @@ internal sealed class RuntimeInventoryReconciler : IRuntimeInventoryReconciler
|
||||
return trimmed.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string NormalizeLegacyDigest(string digest)
|
||||
=> digest.Contains(':', StringComparison.Ordinal)
|
||||
? digest.Trim()
|
||||
: $"sha256:{digest.Trim()}";
|
||||
|
||||
private static string MapLegacyTypeSegment(ArtifactDocumentType type) => type switch
|
||||
{
|
||||
ArtifactDocumentType.LayerBom => "layerbom",
|
||||
ArtifactDocumentType.ImageBom => "imagebom",
|
||||
ArtifactDocumentType.Index => "index",
|
||||
ArtifactDocumentType.Attestation => "attestation",
|
||||
ArtifactDocumentType.SurfaceManifest => "surface-manifest",
|
||||
ArtifactDocumentType.SurfaceEntryTrace => "surface-entrytrace",
|
||||
ArtifactDocumentType.SurfaceLayerFragment => "surface-layer-fragment",
|
||||
ArtifactDocumentType.Diff => "diff",
|
||||
_ => type.ToString().ToLowerInvariant()
|
||||
};
|
||||
|
||||
private static string MapLegacyFormatSegment(ArtifactDocumentFormat format) => format switch
|
||||
{
|
||||
ArtifactDocumentFormat.CycloneDxJson => "cyclonedx-json",
|
||||
ArtifactDocumentFormat.CycloneDxProtobuf => "cyclonedx-protobuf",
|
||||
ArtifactDocumentFormat.SpdxJson => "spdx-json",
|
||||
ArtifactDocumentFormat.BomIndex => "bom-index",
|
||||
ArtifactDocumentFormat.DsseJson => "dsse-json",
|
||||
ArtifactDocumentFormat.SurfaceManifestJson => "surface-manifest-json",
|
||||
ArtifactDocumentFormat.EntryTraceNdjson => "entrytrace-ndjson",
|
||||
ArtifactDocumentFormat.EntryTraceGraphJson => "entrytrace-graph-json",
|
||||
ArtifactDocumentFormat.ComponentFragmentJson => "component-fragment-json",
|
||||
_ => format.ToString().ToLowerInvariant()
|
||||
};
|
||||
|
||||
private static void RecordLatency(Stopwatch stopwatch)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// Description: Service implementation for score replay operations
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using StellaOps.Scanner.Core;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace StellaOps.Scanner.WebService.Services;
|
||||
/// </summary>
|
||||
public sealed class ScoreReplayService : IScoreReplayService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _replayLocks = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly IScanManifestRepository _manifestRepository;
|
||||
private readonly IProofBundleRepository _bundleRepository;
|
||||
private readonly IProofBundleWriter _bundleWriter;
|
||||
@@ -49,52 +50,62 @@ public sealed class ScoreReplayService : IScoreReplayService
|
||||
{
|
||||
_logger.LogInformation("Starting score replay for scan {ScanId}", scanId);
|
||||
|
||||
// Get the manifest
|
||||
var signedManifest = await _manifestRepository.GetManifestAsync(scanId, manifestHash, cancellationToken);
|
||||
if (signedManifest is null)
|
||||
var replayLock = _replayLocks.GetOrAdd(scanId, _ => new SemaphoreSlim(1, 1));
|
||||
await replayLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Manifest not found for scan {ScanId}", scanId);
|
||||
return null;
|
||||
}
|
||||
// Get the manifest
|
||||
var signedManifest = await _manifestRepository.GetManifestAsync(scanId, manifestHash, cancellationToken).ConfigureAwait(false);
|
||||
if (signedManifest is null)
|
||||
{
|
||||
_logger.LogWarning("Manifest not found for scan {ScanId}", scanId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify manifest signature
|
||||
var verifyResult = await _manifestSigner.VerifyAsync(signedManifest, cancellationToken);
|
||||
if (!verifyResult.IsValid)
|
||||
// Verify manifest signature
|
||||
var verifyResult = await _manifestSigner.VerifyAsync(signedManifest, cancellationToken).ConfigureAwait(false);
|
||||
if (!verifyResult.IsValid)
|
||||
{
|
||||
throw new InvalidOperationException($"Manifest signature verification failed: {verifyResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
var manifest = signedManifest.Manifest;
|
||||
|
||||
// Replay scoring with frozen inputs
|
||||
var ledger = new ProofLedger();
|
||||
var score = await _scoringService.ReplayScoreAsync(
|
||||
manifest.ScanId,
|
||||
manifest.ConcelierSnapshotHash,
|
||||
manifest.ExcititorSnapshotHash,
|
||||
manifest.LatticePolicyHash,
|
||||
manifest.Seed,
|
||||
freezeTimestamp ?? manifest.CreatedAtUtc,
|
||||
ledger,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Create proof bundle
|
||||
var bundle = await _bundleWriter.CreateBundleAsync(signedManifest, ledger, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Store bundle reference
|
||||
await _bundleRepository.SaveBundleAsync(bundle, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Score replay complete for scan {ScanId}: score={Score}, rootHash={RootHash}",
|
||||
scanId, score, bundle.RootHash);
|
||||
|
||||
return new ScoreReplayResult(
|
||||
Score: score,
|
||||
RootHash: bundle.RootHash,
|
||||
BundleUri: bundle.BundleUri,
|
||||
ManifestHash: manifest.ComputeHash(),
|
||||
ReplayedAt: DateTimeOffset.UtcNow,
|
||||
Deterministic: manifest.Deterministic);
|
||||
}
|
||||
finally
|
||||
{
|
||||
throw new InvalidOperationException($"Manifest signature verification failed: {verifyResult.ErrorMessage}");
|
||||
replayLock.Release();
|
||||
}
|
||||
|
||||
var manifest = signedManifest.Manifest;
|
||||
|
||||
// Replay scoring with frozen inputs
|
||||
var ledger = new ProofLedger();
|
||||
var score = await _scoringService.ReplayScoreAsync(
|
||||
manifest.ScanId,
|
||||
manifest.ConcelierSnapshotHash,
|
||||
manifest.ExcititorSnapshotHash,
|
||||
manifest.LatticePolicyHash,
|
||||
manifest.Seed,
|
||||
freezeTimestamp ?? manifest.CreatedAtUtc,
|
||||
ledger,
|
||||
cancellationToken);
|
||||
|
||||
// Create proof bundle
|
||||
var bundle = await _bundleWriter.CreateBundleAsync(signedManifest, ledger, cancellationToken);
|
||||
|
||||
// Store bundle reference
|
||||
await _bundleRepository.SaveBundleAsync(bundle, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Score replay complete for scan {ScanId}: score={Score}, rootHash={RootHash}",
|
||||
scanId, score, bundle.RootHash);
|
||||
|
||||
return new ScoreReplayResult(
|
||||
Score: score,
|
||||
RootHash: bundle.RootHash,
|
||||
BundleUri: bundle.BundleUri,
|
||||
ManifestHash: manifest.ComputeHash(),
|
||||
ReplayedAt: DateTimeOffset.UtcNow,
|
||||
Deterministic: manifest.Deterministic);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user