Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.WebService/Endpoints/ProofSpineEndpoints.cs
2026-02-01 21:37:40 +02:00

198 lines
7.3 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Replay.Core;
using StellaOps.Scanner.ProofSpine;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Serialization;
namespace StellaOps.Scanner.WebService.Endpoints;
internal static class ProofSpineEndpoints
{
public static void MapProofSpineEndpoints(this RouteGroupBuilder apiGroup, string spinesSegment, string scansSegment)
{
ArgumentNullException.ThrowIfNull(apiGroup);
var spines = apiGroup.MapGroup(NormalizeSegment(spinesSegment));
spines.MapGet("/{spineId}", HandleGetSpineAsync)
.WithName("scanner.spines.get")
.Produces<ProofSpineResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK, contentType: CborNegotiation.ContentType)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
var scans = apiGroup.MapGroup(NormalizeSegment(scansSegment));
scans.MapGet("/{scanId}/spines", HandleListSpinesAsync)
.WithName("scanner.spines.list-by-scan")
.Produces<ProofSpineListResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK, contentType: CborNegotiation.ContentType)
.RequireAuthorization(ScannerPolicies.ScansRead);
}
private static async Task<IResult> HandleGetSpineAsync(
HttpRequest request,
string spineId,
IProofSpineRepository repository,
ProofSpineVerifier verifier,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(repository);
ArgumentNullException.ThrowIfNull(verifier);
if (string.IsNullOrWhiteSpace(spineId))
{
return Results.NotFound();
}
var spine = await repository.GetByIdAsync(spineId, cancellationToken).ConfigureAwait(false);
if (spine is null)
{
return Results.NotFound();
}
var segments = await repository.GetSegmentsAsync(spineId, cancellationToken).ConfigureAwait(false);
var full = spine with { Segments = segments };
var verification = await verifier.VerifyAsync(full, cancellationToken).ConfigureAwait(false);
var verificationBySegment = verification.Segments.ToDictionary(s => s.SegmentId, s => s, StringComparer.Ordinal);
var dto = new ProofSpineResponseDto
{
SpineId = full.SpineId,
ArtifactId = full.ArtifactId,
VulnerabilityId = full.VulnerabilityId,
PolicyProfileId = full.PolicyProfileId,
Verdict = full.Verdict,
VerdictReason = full.VerdictReason,
RootHash = full.RootHash,
ScanRunId = full.ScanRunId,
CreatedAt = full.CreatedAt,
SupersededBySpineId = full.SupersededBySpineId,
Segments = full.Segments.Select(segment =>
{
verificationBySegment.TryGetValue(segment.SegmentId, out var segmentVerification);
var status = segmentVerification?.Status ?? segment.Status;
return new ProofSegmentDto
{
SegmentId = segment.SegmentId,
SegmentType = ToWireSegmentType(segment.SegmentType),
Index = segment.Index,
InputHash = segment.InputHash,
ResultHash = segment.ResultHash,
PrevSegmentHash = segment.PrevSegmentHash,
Envelope = MapEnvelope(segment.Envelope),
ToolId = segment.ToolId,
ToolVersion = segment.ToolVersion,
Status = ToWireStatus(status),
CreatedAt = segment.CreatedAt,
VerificationErrors = segmentVerification?.Errors.Count > 0 ? segmentVerification.Errors : null
};
}).ToArray(),
Verification = new ProofSpineVerificationDto
{
IsValid = verification.IsValid,
Errors = verification.Errors.ToArray()
}
};
if (CborNegotiation.AcceptsCbor(request))
{
return Results.Bytes(
DeterministicCborSerializer.Serialize(dto),
contentType: CborNegotiation.ContentType);
}
return Results.Ok(dto);
}
private static async Task<IResult> HandleListSpinesAsync(
HttpRequest request,
string scanId,
IProofSpineRepository repository,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(repository);
if (string.IsNullOrWhiteSpace(scanId))
{
var empty = new ProofSpineListResponseDto { Items = Array.Empty<ProofSpineSummaryDto>(), Total = 0 };
if (CborNegotiation.AcceptsCbor(request))
{
return Results.Bytes(
DeterministicCborSerializer.Serialize(empty),
contentType: CborNegotiation.ContentType);
}
return Results.Ok(empty);
}
var summaries = await repository.GetSummariesByScanRunAsync(scanId, cancellationToken).ConfigureAwait(false);
var items = summaries.Select(summary => new ProofSpineSummaryDto
{
SpineId = summary.SpineId,
ArtifactId = summary.ArtifactId,
VulnerabilityId = summary.VulnerabilityId,
Verdict = summary.Verdict,
SegmentCount = summary.SegmentCount,
CreatedAt = summary.CreatedAt
}).ToArray();
var response = new ProofSpineListResponseDto
{
Items = items,
Total = items.Length
};
if (CborNegotiation.AcceptsCbor(request))
{
return Results.Bytes(
DeterministicCborSerializer.Serialize(response),
contentType: CborNegotiation.ContentType);
}
return Results.Ok(response);
}
private static DsseEnvelopeDto MapEnvelope(DsseEnvelope envelope)
=> new()
{
PayloadType = envelope.PayloadType,
Payload = envelope.Payload,
Signatures = envelope.Signatures.Select(signature => new DsseSignatureDto
{
KeyId = signature.KeyId,
Sig = signature.Sig
}).ToArray()
};
private static string ToWireSegmentType(ProofSegmentType type) => type switch
{
ProofSegmentType.SbomSlice => "SBOM_SLICE",
ProofSegmentType.Match => "MATCH",
ProofSegmentType.Reachability => "REACHABILITY",
ProofSegmentType.GuardAnalysis => "GUARD_ANALYSIS",
ProofSegmentType.RuntimeObservation => "RUNTIME_OBSERVATION",
ProofSegmentType.PolicyEval => "POLICY_EVAL",
_ => type.ToString()
};
private static string ToWireStatus(ProofSegmentStatus status)
=> status.ToString().ToLowerInvariant();
private static string NormalizeSegment(string segment)
{
if (string.IsNullOrWhiteSpace(segment))
{
return "/";
}
var trimmed = segment.Trim('/');
return "/" + trimmed;
}
}