update evidence bundle to include new evidence types and implement ProofSpine integration
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Contracts;
|
||||
|
||||
public sealed record ProofSpineListResponseDto
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public IReadOnlyList<ProofSpineSummaryDto> Items { get; init; } = Array.Empty<ProofSpineSummaryDto>();
|
||||
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProofSpineSummaryDto
|
||||
{
|
||||
[JsonPropertyName("spineId")]
|
||||
public string SpineId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("artifactId")]
|
||||
public string ArtifactId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public string VulnerabilityId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("verdict")]
|
||||
public string Verdict { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("segmentCount")]
|
||||
public int SegmentCount { get; init; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProofSpineResponseDto
|
||||
{
|
||||
[JsonPropertyName("spineId")]
|
||||
public string SpineId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("artifactId")]
|
||||
public string ArtifactId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public string VulnerabilityId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("policyProfileId")]
|
||||
public string PolicyProfileId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("verdict")]
|
||||
public string Verdict { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("verdictReason")]
|
||||
public string VerdictReason { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rootHash")]
|
||||
public string RootHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("scanRunId")]
|
||||
public string ScanRunId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("segments")]
|
||||
public IReadOnlyList<ProofSegmentDto> Segments { get; init; } = Array.Empty<ProofSegmentDto>();
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("supersededBySpineId")]
|
||||
public string? SupersededBySpineId { get; init; }
|
||||
|
||||
[JsonPropertyName("verification")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ProofSpineVerificationDto? Verification { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ProofSegmentDto
|
||||
{
|
||||
[JsonPropertyName("segmentId")]
|
||||
public string SegmentId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("segmentType")]
|
||||
public string SegmentType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; init; }
|
||||
|
||||
[JsonPropertyName("inputHash")]
|
||||
public string InputHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("resultHash")]
|
||||
public string ResultHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("prevSegmentHash")]
|
||||
public string? PrevSegmentHash { get; init; }
|
||||
|
||||
[JsonPropertyName("envelope")]
|
||||
public DsseEnvelopeDto Envelope { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("toolId")]
|
||||
public string ToolId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("toolVersion")]
|
||||
public string ToolVersion { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("verificationErrors")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IReadOnlyList<string>? VerificationErrors { get; init; }
|
||||
}
|
||||
|
||||
public sealed record DsseEnvelopeDto
|
||||
{
|
||||
[JsonPropertyName("payloadType")]
|
||||
public string PayloadType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("payload")]
|
||||
public string Payload { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("signatures")]
|
||||
public IReadOnlyList<DsseSignatureDto> Signatures { get; init; } = Array.Empty<DsseSignatureDto>();
|
||||
}
|
||||
|
||||
public sealed record DsseSignatureDto
|
||||
{
|
||||
[JsonPropertyName("keyid")]
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sig")]
|
||||
public string Sig { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record ProofSpineVerificationDto
|
||||
{
|
||||
[JsonPropertyName("isValid")]
|
||||
public bool IsValid { get; init; }
|
||||
|
||||
[JsonPropertyName("errors")]
|
||||
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
|
||||
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.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)
|
||||
.RequireAuthorization(ScannerPolicies.ScansRead);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleGetSpineAsync(
|
||||
string spineId,
|
||||
IProofSpineRepository repository,
|
||||
ProofSpineVerifier verifier,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
return Results.Ok(dto);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleListSpinesAsync(
|
||||
string scanId,
|
||||
IProofSpineRepository repository,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repository);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scanId))
|
||||
{
|
||||
return Results.Ok(new ProofSpineListResponseDto { Items = Array.Empty<ProofSpineSummaryDto>(), Total = 0 });
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return Results.Ok(new ProofSpineListResponseDto
|
||||
{
|
||||
Items = items,
|
||||
Total = items.Length
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,8 @@ public sealed class ScannerWebServiceOptions
|
||||
public string PolicySegment { get; set; } = "policy";
|
||||
|
||||
public string RuntimeSegment { get; set; } = "runtime";
|
||||
|
||||
public string SpinesSegment { get; set; } = "spines";
|
||||
}
|
||||
|
||||
public sealed class ConsoleOptions
|
||||
|
||||
@@ -90,6 +90,11 @@ public static class ScannerWebServiceOptionsValidator
|
||||
throw new InvalidOperationException("API runtimeSegment must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Api.SpinesSegment))
|
||||
{
|
||||
throw new InvalidOperationException("API spinesSegment must be configured.");
|
||||
}
|
||||
|
||||
options.Events ??= new ScannerWebServiceOptions.EventsOptions();
|
||||
ValidateEvents(options.Events);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Plugin.BouncyCastle/StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
|
||||
<ProjectReference Include="../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Cache/StellaOps.Scanner.Cache.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Storage/StellaOps.Scanner.Storage.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user