up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 07:47:08 +02:00
parent 56e2f64d07
commit 1c782897f7
184 changed files with 8991 additions and 649 deletions

View File

@@ -0,0 +1,7 @@
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record ReplayAttachRequest(
string ManifestHash,
IReadOnlyList<ReplayBundleStatusDto> Bundles);
public sealed record ReplayAttachResponse(string Status);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
{

View File

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

View File

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

View File

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

View File

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

View File

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