up
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
public sealed record ReplayAttachRequest(
|
||||
string ManifestHash,
|
||||
IReadOnlyList<ReplayBundleStatusDto> Bundles);
|
||||
|
||||
public sealed record ReplayAttachResponse(string Status);
|
||||
@@ -7,8 +7,19 @@ public sealed record ScanStatusResponse(
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt,
|
||||
string? FailureReason,
|
||||
SurfacePointersDto? Surface);
|
||||
SurfacePointersDto? Surface,
|
||||
ReplayStatusDto? Replay);
|
||||
|
||||
public sealed record ScanStatusTarget(
|
||||
string? Reference,
|
||||
string? Digest);
|
||||
|
||||
public sealed record ReplayStatusDto(
|
||||
string ManifestHash,
|
||||
IReadOnlyList<ReplayBundleStatusDto> Bundles);
|
||||
|
||||
public sealed record ReplayBundleStatusDto(
|
||||
string Type,
|
||||
string Digest,
|
||||
string CasUri,
|
||||
long SizeBytes);
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
namespace StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
public sealed record ScanSnapshot(
|
||||
ScanId ScanId,
|
||||
ScanTarget Target,
|
||||
ScanStatus Status,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt,
|
||||
string? FailureReason);
|
||||
public sealed record ScanSnapshot(
|
||||
ScanId ScanId,
|
||||
ScanTarget Target,
|
||||
ScanStatus Status,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt,
|
||||
string? FailureReason,
|
||||
ReplayArtifacts? Replay);
|
||||
|
||||
public sealed record ReplayArtifacts(
|
||||
string ManifestHash,
|
||||
IReadOnlyList<ReplayBundleSummary> Bundles);
|
||||
|
||||
public sealed record ReplayBundleSummary(
|
||||
string Type,
|
||||
string Digest,
|
||||
string CasUri,
|
||||
long SizeBytes);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
internal static class ReplayEndpoints
|
||||
{
|
||||
public static void MapReplayEndpoints(this RouteGroupBuilder apiGroup)
|
||||
{
|
||||
var replay = apiGroup.MapGroup("/replay");
|
||||
|
||||
replay.MapPost("/{scanId}/attach", HandleAttachAsync)
|
||||
.WithName("scanner.replay.attach")
|
||||
.Produces<ReplayAttachResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAttachAsync(
|
||||
string scanId,
|
||||
ReplayAttachRequest request,
|
||||
IScanCoordinator coordinator,
|
||||
HttpContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ScanId.TryParse(scanId, out var parsed))
|
||||
{
|
||||
return Results.BadRequest("invalid scan id");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ManifestHash) || request.Bundles is null || request.Bundles.Count == 0)
|
||||
{
|
||||
return Results.BadRequest("manifest hash and bundles are required");
|
||||
}
|
||||
|
||||
var replay = new ReplayArtifacts(
|
||||
request.ManifestHash,
|
||||
request.Bundles
|
||||
.Select(b => new ReplayBundleSummary(b.Type, b.Digest, b.CasUri, b.SizeBytes))
|
||||
.ToList());
|
||||
|
||||
var attached = await coordinator.AttachReplayAsync(parsed, replay, cancellationToken).ConfigureAwait(false);
|
||||
if (!attached)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new ReplayAttachResponse("attached"));
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,8 @@ internal static class ScanEndpoints
|
||||
CreatedAt: snapshot.CreatedAt,
|
||||
UpdatedAt: snapshot.UpdatedAt,
|
||||
FailureReason: snapshot.FailureReason,
|
||||
Surface: surfacePointers);
|
||||
Surface: surfacePointers,
|
||||
Replay: snapshot.Replay is null ? null : MapReplay(snapshot.Replay));
|
||||
|
||||
return Json(response, StatusCodes.Status200OK);
|
||||
}
|
||||
@@ -283,6 +284,15 @@ internal static class ScanEndpoints
|
||||
return Results.Empty;
|
||||
}
|
||||
|
||||
private static ReplayStatusDto MapReplay(ReplayArtifacts replay)
|
||||
{
|
||||
return new ReplayStatusDto(
|
||||
ManifestHash: replay.ManifestHash,
|
||||
Bundles: replay.Bundles
|
||||
.Select(b => new ReplayBundleStatusDto(b.Type, b.Digest, b.CasUri, b.SizeBytes))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
|
||||
private static async Task<IResult> HandleEntryTraceAsync(
|
||||
string scanId,
|
||||
|
||||
@@ -31,9 +31,11 @@ using StellaOps.Scanner.WebService.Hosting;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
using StellaOps.Scanner.WebService.Replay;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Extensions;
|
||||
using StellaOps.Scanner.Storage.Mongo;
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -83,13 +85,14 @@ builder.Services.AddScannerCache(builder.Configuration);
|
||||
builder.Services.AddSingleton<ServiceStatus>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddSingleton<ScanProgressStream>();
|
||||
builder.Services.AddSingleton<IScanProgressPublisher>(sp => sp.GetRequiredService<ScanProgressStream>());
|
||||
builder.Services.AddSingleton<IScanProgressReader>(sp => sp.GetRequiredService<ScanProgressStream>());
|
||||
builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();
|
||||
builder.Services.AddSingleton<IPolicySnapshotRepository, InMemoryPolicySnapshotRepository>();
|
||||
builder.Services.AddSingleton<IPolicyAuditRepository, InMemoryPolicyAuditRepository>();
|
||||
builder.Services.AddSingleton<IScanProgressPublisher>(sp => sp.GetRequiredService<ScanProgressStream>());
|
||||
builder.Services.AddSingleton<IScanProgressReader>(sp => sp.GetRequiredService<ScanProgressStream>());
|
||||
builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();
|
||||
builder.Services.AddSingleton<IPolicySnapshotRepository, InMemoryPolicySnapshotRepository>();
|
||||
builder.Services.AddSingleton<IPolicyAuditRepository, InMemoryPolicyAuditRepository>();
|
||||
builder.Services.AddSingleton<PolicySnapshotStore>();
|
||||
builder.Services.AddSingleton<PolicyPreviewService>();
|
||||
builder.Services.AddSingleton<IRecordModeService, RecordModeService>();
|
||||
builder.Services.AddStellaOpsCrypto();
|
||||
builder.Services.AddBouncyCastleEd25519Provider();
|
||||
builder.Services.AddSingleton<IReportSigner, ReportSigner>();
|
||||
@@ -386,6 +389,7 @@ if (app.Environment.IsEnvironment("Testing"))
|
||||
}
|
||||
|
||||
apiGroup.MapScanEndpoints(resolvedOptions.Api.ScansSegment);
|
||||
apiGroup.MapReplayEndpoints();
|
||||
|
||||
if (resolvedOptions.Features.EnablePolicyPreview)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Replay;
|
||||
|
||||
internal interface IRecordModeService
|
||||
{
|
||||
Task<(ReplayRunRecord Run, IReadOnlyList<ReplayBundleRecord> Bundles)> BuildAsync(
|
||||
string scanId,
|
||||
ReplayManifest manifest,
|
||||
ReplayBundleWriteResult inputBundle,
|
||||
ReplayBundleWriteResult outputBundle,
|
||||
string sbomDigest,
|
||||
string findingsDigest,
|
||||
string? vexDigest = null,
|
||||
string? logDigest = null,
|
||||
IEnumerable<(ReplayBundleWriteResult Result, string Type)>? additionalBundles = null);
|
||||
|
||||
Task<ReplayArtifacts?> AttachAsync(
|
||||
ScanId scanId,
|
||||
ReplayManifest manifest,
|
||||
ReplayBundleWriteResult inputBundle,
|
||||
ReplayBundleWriteResult outputBundle,
|
||||
string sbomDigest,
|
||||
string findingsDigest,
|
||||
IScanCoordinator coordinator,
|
||||
string? vexDigest = null,
|
||||
string? logDigest = null,
|
||||
IEnumerable<(ReplayBundleWriteResult Result, string Type)>? additionalBundles = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.Core.Replay;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Replay;
|
||||
|
||||
/// <summary>
|
||||
/// Prepares replay run metadata from WebService scan results. This is a thin façade that will be invoked
|
||||
/// once record-mode wiring lands in the scan pipeline.
|
||||
/// </summary>
|
||||
internal sealed class RecordModeService : IRecordModeService
|
||||
{
|
||||
private readonly RecordModeAssembler _assembler;
|
||||
|
||||
public RecordModeService(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_assembler = new RecordModeAssembler(timeProvider);
|
||||
}
|
||||
|
||||
public Task<(ReplayRunRecord Run, IReadOnlyList<ReplayBundleRecord> Bundles)> BuildAsync(
|
||||
string scanId,
|
||||
ReplayManifest manifest,
|
||||
ReplayBundleWriteResult inputBundle,
|
||||
ReplayBundleWriteResult outputBundle,
|
||||
string sbomDigest,
|
||||
string findingsDigest,
|
||||
string? vexDigest = null,
|
||||
string? logDigest = null,
|
||||
IEnumerable<(ReplayBundleWriteResult Result, string Type)>? additionalBundles = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
|
||||
var run = _assembler.BuildRun(scanId, manifest, sbomDigest, findingsDigest, vexDigest, logDigest);
|
||||
var bundles = _assembler.BuildBundles(inputBundle, outputBundle, additionalBundles);
|
||||
|
||||
return Task.FromResult((run, bundles));
|
||||
}
|
||||
|
||||
public async Task<ReplayArtifacts?> AttachAsync(
|
||||
ScanId scanId,
|
||||
ReplayManifest manifest,
|
||||
ReplayBundleWriteResult inputBundle,
|
||||
ReplayBundleWriteResult outputBundle,
|
||||
string sbomDigest,
|
||||
string findingsDigest,
|
||||
IScanCoordinator coordinator,
|
||||
string? vexDigest = null,
|
||||
string? logDigest = null,
|
||||
IEnumerable<(ReplayBundleWriteResult Result, string Type)>? additionalBundles = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(coordinator);
|
||||
|
||||
var (run, bundles) = await BuildAsync(
|
||||
scanId.Value,
|
||||
manifest,
|
||||
inputBundle,
|
||||
outputBundle,
|
||||
sbomDigest,
|
||||
findingsDigest,
|
||||
vexDigest,
|
||||
logDigest,
|
||||
additionalBundles).ConfigureAwait(false);
|
||||
|
||||
var replay = BuildArtifacts(run.ManifestHash, bundles);
|
||||
var attached = await coordinator.AttachReplayAsync(scanId, replay, cancellationToken).ConfigureAwait(false);
|
||||
return attached ? replay : null;
|
||||
}
|
||||
|
||||
private static ReplayArtifacts BuildArtifacts(string manifestHash, IReadOnlyList<ReplayBundleRecord> bundles)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(manifestHash);
|
||||
ArgumentNullException.ThrowIfNull(bundles);
|
||||
|
||||
var summaries = bundles
|
||||
.Select(bundle => new ReplayBundleSummary(
|
||||
bundle.Type,
|
||||
NormalizeDigest(bundle.Id),
|
||||
bundle.Location,
|
||||
bundle.Size))
|
||||
.ToList();
|
||||
|
||||
return new ReplayArtifacts(manifestHash, summaries);
|
||||
}
|
||||
|
||||
private static string NormalizeDigest(string digest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = digest.Trim().ToLowerInvariant();
|
||||
return trimmed.StartsWith("sha256:", StringComparison.Ordinal)
|
||||
? trimmed
|
||||
: $"sha256:{trimmed}";
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,6 @@ public interface IScanCoordinator
|
||||
ValueTask<ScanSnapshot?> GetAsync(ScanId scanId, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<ScanSnapshot?> TryFindByTargetAsync(string? reference, string? digest, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<bool> AttachReplayAsync(ScanId scanId, ReplayArtifacts replay, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -46,8 +46,9 @@ public sealed class InMemoryScanCoordinator : IScanCoordinator
|
||||
normalizedTarget,
|
||||
ScanStatus.Pending,
|
||||
now,
|
||||
now,
|
||||
null)),
|
||||
now,
|
||||
null,
|
||||
null)),
|
||||
(_, existing) =>
|
||||
{
|
||||
if (submission.Force)
|
||||
@@ -72,8 +73,8 @@ public sealed class InMemoryScanCoordinator : IScanCoordinator
|
||||
return ValueTask.FromResult(new ScanSubmissionResult(entry.Snapshot, created));
|
||||
}
|
||||
|
||||
public ValueTask<ScanSnapshot?> GetAsync(ScanId scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
public ValueTask<ScanSnapshot?> GetAsync(ScanId scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (scans.TryGetValue(scanId.Value, out var entry))
|
||||
{
|
||||
return ValueTask.FromResult<ScanSnapshot?>(entry.Snapshot);
|
||||
@@ -109,6 +110,30 @@ public sealed class InMemoryScanCoordinator : IScanCoordinator
|
||||
return ValueTask.FromResult<ScanSnapshot?>(null);
|
||||
}
|
||||
|
||||
public ValueTask<bool> AttachReplayAsync(ScanId scanId, ReplayArtifacts replay, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(replay);
|
||||
|
||||
if (!scans.TryGetValue(scanId.Value, out var existing))
|
||||
{
|
||||
return ValueTask.FromResult(false);
|
||||
}
|
||||
|
||||
var updated = existing.Snapshot with
|
||||
{
|
||||
Replay = replay,
|
||||
UpdatedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
scans[scanId.Value] = new ScanEntry(updated);
|
||||
progressPublisher.Publish(scanId, updated.Status.ToString(), "replay-attached", new Dictionary<string, object?>
|
||||
{
|
||||
["replay.manifest"] = replay.ManifestHash,
|
||||
["replay.bundleCount"] = replay.Bundles.Count
|
||||
});
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
private void IndexTarget(string scanId, ScanTarget target)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(target.Digest))
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="../../Zastava/__Libraries/StellaOps.Zastava.Core/StellaOps.Zastava.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user