up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 20:23:28 +02:00
parent 4831c7fcb0
commit d63af51f84
139 changed files with 8010 additions and 2795 deletions

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.WebService.Contracts;
public sealed record EntropyLayerRequest(
[property: JsonPropertyName("layerDigest")] string LayerDigest,
[property: JsonPropertyName("opaqueRatio")] double OpaqueRatio,
[property: JsonPropertyName("opaqueBytes")] long OpaqueBytes,
[property: JsonPropertyName("totalBytes")] long TotalBytes);
public sealed record EntropyIngestRequest(
[property: JsonPropertyName("imageOpaqueRatio")] double ImageOpaqueRatio,
[property: JsonPropertyName("layers")] IReadOnlyList<EntropyLayerRequest> Layers);

View File

@@ -7,6 +7,7 @@ public sealed record ScanStatusResponse(
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
string? FailureReason,
EntropyStatusDto? Entropy,
SurfacePointersDto? Surface,
ReplayStatusDto? Replay);
@@ -14,6 +15,16 @@ public sealed record ScanStatusTarget(
string? Reference,
string? Digest);
public sealed record EntropyStatusDto(
double ImageOpaqueRatio,
IReadOnlyList<EntropyLayerStatusDto> Layers);
public sealed record EntropyLayerStatusDto(
string LayerDigest,
double OpaqueRatio,
long OpaqueBytes,
long TotalBytes);
public sealed record ReplayStatusDto(
string ManifestHash,
IReadOnlyList<ReplayBundleStatusDto> Bundles);

View File

@@ -7,6 +7,7 @@ public sealed record ScanSnapshot(
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
string? FailureReason,
EntropySnapshot? Entropy,
ReplayArtifacts? Replay);
public sealed record ReplayArtifacts(
@@ -18,3 +19,13 @@ public sealed record ReplayBundleSummary(
string Digest,
string CasUri,
long SizeBytes);
public sealed record EntropySnapshot(
double ImageOpaqueRatio,
IReadOnlyList<EntropyLayerSnapshot> Layers);
public sealed record EntropyLayerSnapshot(
string LayerDigest,
double OpaqueRatio,
long OpaqueBytes,
long TotalBytes);

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text.Json;
@@ -45,6 +46,13 @@ internal static class ScanEndpoints
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
scans.MapPost("/{scanId}/entropy", HandleAttachEntropyAsync)
.WithName("scanner.scans.entropy")
.Produces(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansWrite);
scans.MapGet("/{scanId}/events", HandleProgressStreamAsync)
.WithName("scanner.scans.events")
.Produces(StatusCodes.Status200OK)
@@ -203,12 +211,79 @@ internal static class ScanEndpoints
CreatedAt: snapshot.CreatedAt,
UpdatedAt: snapshot.UpdatedAt,
FailureReason: snapshot.FailureReason,
Entropy: snapshot.Entropy is null
? null
: new EntropyStatusDto(
snapshot.Entropy.ImageOpaqueRatio,
snapshot.Entropy.Layers
.Select(l => new EntropyLayerStatusDto(l.LayerDigest, l.OpaqueRatio, l.OpaqueBytes, l.TotalBytes))
.ToArray()),
Surface: surfacePointers,
Replay: snapshot.Replay is null ? null : MapReplay(snapshot.Replay));
return Json(response, StatusCodes.Status200OK);
}
private static async Task<IResult> HandleAttachEntropyAsync(
string scanId,
EntropyIngestRequest request,
IScanCoordinator coordinator,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(request);
if (!ScanId.TryParse(scanId, out var parsed))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
}
if (request.Layers is null || request.Layers.Count == 0)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Entropy layers are required",
StatusCodes.Status400BadRequest);
}
var layers = request.Layers
.Where(l => !string.IsNullOrWhiteSpace(l.LayerDigest))
.Select(l => new EntropyLayerSnapshot(
l.LayerDigest.Trim(),
l.OpaqueRatio,
l.OpaqueBytes,
l.TotalBytes))
.ToArray();
if (layers.Length == 0)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Entropy layers are required",
StatusCodes.Status400BadRequest);
}
var snapshot = new EntropySnapshot(
request.ImageOpaqueRatio,
layers);
var attached = await coordinator.AttachEntropyAsync(parsed, snapshot, cancellationToken).ConfigureAwait(false);
if (!attached)
{
return Results.NotFound();
}
return Results.Accepted();
}
private static async Task<IResult> HandleProgressStreamAsync(
string scanId,
string? format,

View File

@@ -11,4 +11,6 @@ public interface IScanCoordinator
ValueTask<ScanSnapshot?> TryFindByTargetAsync(string? reference, string? digest, CancellationToken cancellationToken);
ValueTask<bool> AttachReplayAsync(ScanId scanId, ReplayArtifacts replay, CancellationToken cancellationToken);
ValueTask<bool> AttachEntropyAsync(ScanId scanId, EntropySnapshot entropy, CancellationToken cancellationToken);
}

View File

@@ -45,15 +45,16 @@ public sealed class InMemoryScanCoordinator : IScanCoordinator
scanId,
normalizedTarget,
ScanStatus.Pending,
now,
now,
now,
null,
null,
null)),
(_, existing) =>
{
if (submission.Force)
{
var snapshot = existing.Snapshot with
(_, existing) =>
{
if (submission.Force)
{
var snapshot = existing.Snapshot with
{
Status = ScanStatus.Pending,
UpdatedAt = now,
@@ -134,6 +135,30 @@ public sealed class InMemoryScanCoordinator : IScanCoordinator
return ValueTask.FromResult(true);
}
public ValueTask<bool> AttachEntropyAsync(ScanId scanId, EntropySnapshot entropy, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(entropy);
if (!scans.TryGetValue(scanId.Value, out var existing))
{
return ValueTask.FromResult(false);
}
var updated = existing.Snapshot with
{
Entropy = entropy,
UpdatedAt = timeProvider.GetUtcNow()
};
scans[scanId.Value] = new ScanEntry(updated);
progressPublisher.Publish(scanId, updated.Status.ToString(), "entropy-attached", new Dictionary<string, object?>
{
["entropy.imageOpaqueRatio"] = entropy.ImageOpaqueRatio,
["entropy.layers"] = entropy.Layers.Count
});
return ValueTask.FromResult(true);
}
private void IndexTarget(string scanId, ScanTarget target)
{
if (!string.IsNullOrWhiteSpace(target.Digest))