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

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