up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 07:46:56 +02:00
parent d63af51f84
commit ea970ead2a
302 changed files with 43161 additions and 1534 deletions

View File

@@ -32,4 +32,9 @@ internal interface IRecordModeService
string? logDigest = null,
IEnumerable<(ReplayBundleWriteResult Result, string Type)>? additionalBundles = null,
CancellationToken cancellationToken = default);
Task<RecordModeResult> RecordAsync(
RecordModeRequest request,
IScanCoordinator coordinator,
CancellationToken cancellationToken = default);
}

View File

@@ -1,10 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Replay.Core;
using StellaOps.Scanner.Core.Replay;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.ObjectStore;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;
@@ -17,10 +24,32 @@ namespace StellaOps.Scanner.WebService.Replay;
internal sealed class RecordModeService : IRecordModeService
{
private readonly RecordModeAssembler _assembler;
private readonly ReachabilityReplayWriter _reachability;
private readonly IArtifactObjectStore? _objectStore;
private readonly ScannerStorageOptions? _storageOptions;
private readonly TimeProvider _timeProvider;
private readonly ILogger<RecordModeService>? _logger;
public RecordModeService(
IArtifactObjectStore objectStore,
IOptions<ScannerStorageOptions> storageOptions,
TimeProvider timeProvider,
ILogger<RecordModeService> logger)
{
_objectStore = objectStore ?? throw new ArgumentNullException(nameof(objectStore));
_storageOptions = (storageOptions ?? throw new ArgumentNullException(nameof(storageOptions))).Value;
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_assembler = new RecordModeAssembler(timeProvider);
_reachability = new ReachabilityReplayWriter();
}
// Legacy/testing constructor for unit tests that do not require storage.
public RecordModeService(TimeProvider? timeProvider = null)
{
_assembler = new RecordModeAssembler(timeProvider);
_reachability = new ReachabilityReplayWriter();
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<(ReplayRunRecord Run, IReadOnlyList<ReplayBundleRecord> Bundles)> BuildAsync(
@@ -73,6 +102,50 @@ internal sealed class RecordModeService : IRecordModeService
return attached ? replay : null;
}
public async Task<RecordModeResult> RecordAsync(
RecordModeRequest request,
IScanCoordinator coordinator,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(coordinator);
if (_objectStore is null || _storageOptions is null)
{
throw new InvalidOperationException("Record mode storage dependencies are not configured.");
}
var manifest = BuildManifest(request);
var inputEntries = BuildInputBundleEntries(request, manifest);
var outputEntries = BuildOutputBundleEntries(request);
var inputBundle = await StoreBundleAsync(inputEntries, "replay/input", cancellationToken).ConfigureAwait(false);
var outputBundle = await StoreBundleAsync(outputEntries, "replay/output", cancellationToken).ConfigureAwait(false);
var additional = BuildAdditionalBundles(request);
var (run, bundles) = await BuildAsync(
request.ScanId,
manifest,
inputBundle,
outputBundle,
request.SbomDigest,
request.FindingsDigest,
request.VexDigest,
request.LogDigest,
additional).ConfigureAwait(false);
var replay = BuildArtifacts(run.ManifestHash, bundles);
var attached = await coordinator.AttachReplayAsync(new ScanId(request.ScanId), replay, cancellationToken).ConfigureAwait(false);
if (!attached)
{
throw new InvalidOperationException("Unable to attach replay artifacts to scan.");
}
return new RecordModeResult(manifest, run, replay);
}
private static ReplayArtifacts BuildArtifacts(string manifestHash, IReadOnlyList<ReplayBundleRecord> bundles)
{
ArgumentException.ThrowIfNullOrWhiteSpace(manifestHash);
@@ -101,4 +174,132 @@ internal sealed class RecordModeService : IRecordModeService
? trimmed
: $"sha256:{trimmed}";
}
private ReplayManifest BuildManifest(RecordModeRequest request)
{
var manifest = new ReplayManifest
{
SchemaVersion = ReplayManifestVersions.V1,
Scan = new ReplayScanMetadata
{
Id = request.ScanId,
Time = request.ScanTime ?? _timeProvider.GetUtcNow(),
PolicyDigest = request.PolicyDigest,
FeedSnapshot = request.FeedSnapshot,
Toolchain = request.Toolchain,
AnalyzerSetDigest = request.AnalyzerSetDigest
},
Reachability = new ReplayReachabilitySection
{
AnalysisId = request.ReachabilityAnalysisId
}
};
_reachability.AttachEvidence(manifest, request.ReachabilityGraphs, request.ReachabilityTraces);
return manifest;
}
private static List<ReplayBundleEntry> BuildInputBundleEntries(RecordModeRequest request, ReplayManifest manifest)
{
var entries = new List<ReplayBundleEntry>
{
new("manifest/replay.json", manifest.ToCanonicalJson()),
new("inputs/policy.digest", Encoding.UTF8.GetBytes(request.PolicyDigest ?? string.Empty)),
new("inputs/feed.snapshot", Encoding.UTF8.GetBytes(request.FeedSnapshot ?? string.Empty)),
new("inputs/toolchain.txt", Encoding.UTF8.GetBytes(request.Toolchain ?? string.Empty)),
new("inputs/analyzers.digest", Encoding.UTF8.GetBytes(request.AnalyzerSetDigest ?? string.Empty)),
new("inputs/image.digest", Encoding.UTF8.GetBytes(request.ImageDigest ?? string.Empty))
};
return entries;
}
private static List<ReplayBundleEntry> BuildOutputBundleEntries(RecordModeRequest request)
{
var entries = new List<ReplayBundleEntry>
{
new("outputs/sbom.json", request.Sbom),
new("outputs/findings.ndjson", request.Findings)
};
if (!request.Vex.IsEmpty)
{
entries.Add(new ReplayBundleEntry("outputs/vex.json", request.Vex));
}
if (!request.Log.IsEmpty)
{
entries.Add(new ReplayBundleEntry("outputs/log.ndjson", request.Log));
}
return entries;
}
private async Task<ReplayBundleWriteResult> StoreBundleAsync(
IReadOnlyCollection<ReplayBundleEntry> entries,
string casPrefix,
CancellationToken cancellationToken)
{
using var buffer = new MemoryStream();
var result = await ReplayBundleWriter.WriteTarZstAsync(entries, buffer, casPrefix: casPrefix, cancellationToken: cancellationToken).ConfigureAwait(false);
buffer.Position = 0;
var key = BuildReplayKey(result.ZstSha256, _storageOptions!.ObjectStore.RootPrefix, casPrefix);
var descriptor = new ArtifactObjectDescriptor(
_storageOptions.ObjectStore.BucketName,
key,
Immutable: true,
RetainFor: _storageOptions.ObjectStore.ComplianceRetention);
await _objectStore!.PutAsync(descriptor, buffer, cancellationToken).ConfigureAwait(false);
_logger?.LogInformation("Stored replay bundle {Digest} at {Key}", result.ZstSha256, key);
return result;
}
private static IReadOnlyList<(ReplayBundleWriteResult Result, string Type)>? BuildAdditionalBundles(RecordModeRequest request)
=> request.AdditionalBundles is null ? null : request.AdditionalBundles.ToList();
private static string BuildReplayKey(string sha256, string? rootPrefix, string casPrefix)
{
var head = sha256[..2];
var prefix = string.IsNullOrWhiteSpace(rootPrefix) ? string.Empty : rootPrefix.Trim().TrimEnd('/') + "/";
var cas = string.IsNullOrWhiteSpace(casPrefix) ? "replay" : casPrefix.Trim('/');
return $"{prefix}{cas}/{head}/{sha256}.tar.zst";
}
}
public sealed record RecordModeRequest(
string ScanId,
string ImageDigest,
string SbomDigest,
string FindingsDigest,
ReadOnlyMemory<byte> Sbom,
ReadOnlyMemory<byte> Findings,
ReadOnlyMemory<byte> Vex,
ReadOnlyMemory<byte> Log)
{
public string? PolicyDigest { get; init; }
public string? FeedSnapshot { get; init; }
public string? Toolchain { get; init; }
public string? AnalyzerSetDigest { get; init; }
public string? ReachabilityAnalysisId { get; init; }
public IEnumerable<ReachabilityReplayGraph>? ReachabilityGraphs { get; init; }
public IEnumerable<ReachabilityReplayTrace>? ReachabilityTraces { get; init; }
public DateTimeOffset? ScanTime { get; init; }
public IEnumerable<(ReplayBundleWriteResult Result, string Type)>? AdditionalBundles { get; init; }
}
public sealed record RecordModeResult(
ReplayManifest Manifest,
ReplayRunRecord Run,
ReplayArtifacts Artifacts);