198 lines
7.3 KiB
C#
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;
|
|
}
|
|
}
|