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
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user