save work

This commit is contained in:
StellaOps Bot
2025-12-19 07:28:23 +02:00
parent 6410a6d082
commit 2eafe98d44
97 changed files with 5040 additions and 1443 deletions

View File

@@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.Core;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Services;
@@ -85,7 +86,7 @@ internal static class ScoreReplayEndpoints
RootHash: result.RootHash,
BundleUri: result.BundleUri,
ManifestHash: result.ManifestHash,
ReplayedAtUtc: result.ReplayedAt,
ReplayedAt: result.ReplayedAt,
Deterministic: result.Deterministic));
}
catch (InvalidOperationException ex)
@@ -107,6 +108,8 @@ internal static class ScoreReplayEndpoints
string scanId,
[FromQuery] string? rootHash,
IScoreReplayService replayService,
IProofBundleWriter bundleWriter,
IScanManifestSigner manifestSigner,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(scanId))
@@ -131,11 +134,29 @@ internal static class ScoreReplayEndpoints
});
}
bool manifestDsseValid;
try
{
var contents = await bundleWriter.ReadBundleAsync(bundle.BundleUri, cancellationToken).ConfigureAwait(false);
var verify = await manifestSigner.VerifyAsync(contents.SignedManifest, cancellationToken).ConfigureAwait(false);
manifestDsseValid = verify.IsValid;
}
catch (FileNotFoundException ex)
{
return Results.NotFound(new ProblemDetails
{
Title = "Bundle not found",
Detail = ex.Message,
Status = StatusCodes.Status404NotFound
});
}
return Results.Ok(new ScoreBundleResponse(
ScanId: bundle.ScanId,
RootHash: bundle.RootHash,
BundleUri: bundle.BundleUri,
CreatedAtUtc: bundle.CreatedAtUtc));
ManifestDsseValid: manifestDsseValid,
CreatedAt: bundle.CreatedAtUtc));
}
/// <summary>
@@ -213,14 +234,14 @@ public sealed record ScoreReplayRequest(
/// <param name="RootHash">Root hash of the proof ledger.</param>
/// <param name="BundleUri">URI to the proof bundle.</param>
/// <param name="ManifestHash">Hash of the manifest used.</param>
/// <param name="ReplayedAtUtc">When the replay was performed.</param>
/// <param name="ReplayedAt">When the replay was performed.</param>
/// <param name="Deterministic">Whether the replay was deterministic.</param>
public sealed record ScoreReplayResponse(
double Score,
string RootHash,
string BundleUri,
string ManifestHash,
DateTimeOffset ReplayedAtUtc,
DateTimeOffset ReplayedAt,
bool Deterministic);
/// <summary>
@@ -230,7 +251,8 @@ public sealed record ScoreBundleResponse(
string ScanId,
string RootHash,
string BundleUri,
DateTimeOffset CreatedAtUtc);
bool ManifestDsseValid,
DateTimeOffset CreatedAt);
/// <summary>
/// Request for bundle verification.

View File

@@ -92,6 +92,11 @@ public sealed class ScannerWebServiceOptions
/// </summary>
public DeterminismOptions Determinism { get; set; } = new();
/// <summary>
/// Score replay configuration (disabled by default).
/// </summary>
public ScoreReplayOptions ScoreReplay { get; set; } = new();
public sealed class StorageOptions
{
public string Driver { get; set; } = "postgres";
@@ -440,4 +445,19 @@ public sealed class ScannerWebServiceOptions
public string? PolicySnapshotId { get; set; }
}
public sealed class ScoreReplayOptions
{
/// <summary>
/// Enables score replay endpoints (/api/v1/score/*).
/// Default: false.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Directory used to persist proof bundles created during replay.
/// When empty, the host selects a safe default (tests use temp storage).
/// </summary>
public string BundleStoragePath { get; set; } = string.Empty;
}
}

View File

@@ -23,6 +23,7 @@ using StellaOps.Cryptography.Plugin.BouncyCastle;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Policy;
using StellaOps.Scanner.Cache;
using StellaOps.Scanner.Core;
using StellaOps.Scanner.Core.Configuration;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.TrustAnchors;
@@ -124,6 +125,27 @@ builder.Services.AddSingleton<IPolicyAuditRepository, InMemoryPolicyAuditReposit
builder.Services.AddSingleton<PolicySnapshotStore>();
builder.Services.AddSingleton<PolicyPreviewService>();
builder.Services.AddSingleton<IRecordModeService, RecordModeService>();
builder.Services.AddSingleton<IScoreReplayService, ScoreReplayService>();
builder.Services.AddSingleton<IScanManifestRepository, InMemoryScanManifestRepository>();
builder.Services.AddSingleton<IProofBundleRepository, InMemoryProofBundleRepository>();
builder.Services.AddSingleton<IScoringService, DeterministicScoringService>();
builder.Services.AddSingleton<IScanManifestSigner, ScanManifestSigner>();
builder.Services.AddSingleton<IProofBundleWriter>(sp =>
{
var options = sp.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
var hostEnvironment = sp.GetRequiredService<IHostEnvironment>();
var configuredPath = options.ScoreReplay.BundleStoragePath?.Trim() ?? string.Empty;
var defaultPath = hostEnvironment.IsEnvironment("Testing")
? Path.Combine(Path.GetTempPath(), "stellaops-proofs-testing")
: Path.Combine(Path.GetTempPath(), "stellaops-proofs");
return new ProofBundleWriter(new ProofBundleWriterOptions
{
StorageBasePath = string.IsNullOrWhiteSpace(configuredPath) ? defaultPath : configuredPath,
ContentAddressed = true
});
});
builder.Services.AddReachabilityDrift();
builder.Services.AddStellaOpsCrypto();
builder.Services.AddBouncyCastleEd25519Provider();
@@ -470,7 +492,12 @@ apiGroup.MapScanEndpoints(resolvedOptions.Api.ScansSegment);
apiGroup.MapReachabilityDriftRootEndpoints();
apiGroup.MapProofSpineEndpoints(resolvedOptions.Api.SpinesSegment, resolvedOptions.Api.ScansSegment);
apiGroup.MapReplayEndpoints();
if (resolvedOptions.ScoreReplay.Enabled)
{
apiGroup.MapScoreReplayEndpoints();
}
apiGroup.MapWitnessEndpoints(); // Sprint: SPRINT_3700_0001_0001
apiGroup.MapEpssEndpoints(); // Sprint: SPRINT_3410_0002_0001
if (resolvedOptions.Features.EnablePolicyPreview)
{

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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 />

View File

@@ -4,7 +4,8 @@
| --- | --- | --- | --- |
| `SCAN-API-3101-001` | `docs/implplan/archived/SPRINT_3101_0001_0001_scanner_api_standardization.md` | DOING | Align Scanner OpenAPI spec with current endpoints and include ProofSpine routes; compose into `src/Api/StellaOps.Api.OpenApi/stella.yaml`. |
| `PROOFSPINE-3100-API` | `docs/implplan/archived/SPRINT_3100_0001_0001_proof_spine_system.md` | DONE | Implemented and tested `/api/v1/spines/*` endpoints with verification output (CBOR accept tracked in SPRINT_3105). |
| `PROOF-CBOR-3105-001` | `docs/implplan/SPRINT_3105_0001_0001_proofspine_cbor_accept.md` | DOING | Add `Accept: application/cbor` support for ProofSpine endpoints + tests. |
| `PROOF-CBOR-3105-001` | `docs/implplan/SPRINT_3105_0001_0001_proofspine_cbor_accept.md` | DONE | Added `Accept: application/cbor` support for ProofSpine endpoints + tests (`dotnet test src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj -c Release`). |
| `SCAN-AIRGAP-0340-001` | `docs/implplan/SPRINT_0340_0001_0001_scanner_offline_config.md` | DONE | Offline kit import + DSSE/offline Rekor verification wired; integration tests cover success/failure/audit. |
| `DRIFT-3600-API` | `docs/implplan/SPRINT_3600_0003_0001_drift_detection_engine.md` | DONE | Add reachability drift endpoints (`/api/v1/scans/{id}/drift`, `/api/v1/drift/{id}/sinks`) + integration tests. |
| `SCAN-API-3103-001` | `docs/implplan/SPRINT_3103_0001_0001_scanner_api_ingestion_completion.md` | DONE | Implement missing ingestion services + DI for callgraph/SBOM endpoints and add deterministic integration tests. |
| `EPSS-SCAN-011` | `docs/implplan/SPRINT_3410_0002_0001_epss_scanner_integration.md` | DONE | Wired `/api/v1/epss/*` endpoints and added `EpssEndpointsTests` integration coverage. |