doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,273 @@
// -----------------------------------------------------------------------------
// VerdictEndpoints.cs
// Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
// Task: VL-004 - Create POST /verdicts API endpoint
// Description: REST API endpoints for verdict ledger operations
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Attestor.Persistence.Entities;
using StellaOps.Attestor.Services;
namespace StellaOps.Attestor.WebService.Endpoints;
/// <summary>
/// REST API endpoints for the verdict ledger.
/// </summary>
public static class VerdictEndpoints
{
/// <summary>
/// Maps verdict ledger endpoints.
/// </summary>
public static void MapVerdictEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/verdicts")
.WithTags("Verdicts")
.WithOpenApi();
group.MapPost("/", CreateVerdict)
.WithName("CreateVerdict")
.WithSummary("Append a new verdict to the ledger")
.WithDescription("Creates a new verdict entry with cryptographic chain linking")
.Produces<CreateVerdictResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status409Conflict);
group.MapGet("/", QueryVerdicts)
.WithName("QueryVerdicts")
.WithSummary("Query verdicts by bom-ref")
.WithDescription("Returns all verdicts for a given package/artifact reference")
.Produces<IReadOnlyList<VerdictResponse>>();
group.MapGet("/{hash}", GetVerdictByHash)
.WithName("GetVerdictByHash")
.WithSummary("Get a verdict by its hash")
.WithDescription("Returns a specific verdict entry by its SHA-256 hash")
.Produces<VerdictResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/chain/verify", VerifyChain)
.WithName("VerifyChainIntegrity")
.WithSummary("Verify ledger chain integrity")
.WithDescription("Walks the hash chain to verify cryptographic integrity")
.Produces<ChainVerificationResult>();
group.MapGet("/latest", GetLatestVerdict)
.WithName("GetLatestVerdict")
.WithSummary("Get the latest verdict for a bom-ref")
.Produces<VerdictResponse>()
.Produces(StatusCodes.Status404NotFound);
}
private static async Task<IResult> CreateVerdict(
CreateVerdictRequest request,
IVerdictLedgerService service,
HttpContext context,
CancellationToken ct)
{
// Validate request
if (string.IsNullOrEmpty(request.BomRef))
{
return Results.BadRequest(new { error = "bom_ref is required" });
}
if (string.IsNullOrEmpty(request.PolicyBundleId))
{
return Results.BadRequest(new { error = "policy_bundle_id is required" });
}
// TODO: Verify DSSE signature against Authority key roster
// if (!await VerifySignatureAsync(request.Signature, request, ct))
// {
// return Results.Unauthorized();
// }
// Get tenant from context (placeholder - would come from auth)
var tenantId = context.Request.Headers.TryGetValue("X-Tenant-Id", out var tid)
? Guid.Parse(tid.FirstOrDefault() ?? Guid.Empty.ToString())
: Guid.Empty;
try
{
var appendRequest = new AppendVerdictRequest
{
BomRef = request.BomRef,
CycloneDxSerial = request.CycloneDxSerial,
Decision = Enum.TryParse<VerdictDecision>(request.Decision, ignoreCase: true, out var d) ? d : VerdictDecision.Unknown,
Reason = request.Reason,
PolicyBundleId = request.PolicyBundleId,
PolicyBundleHash = request.PolicyBundleHash ?? "",
VerifierImageDigest = request.VerifierImageDigest ?? "",
SignerKeyId = request.SignerKeyId ?? "",
TenantId = tenantId
};
var entry = await service.AppendVerdictAsync(appendRequest, ct);
return Results.Created($"/api/v1/verdicts/{entry.VerdictHash}", new CreateVerdictResponse
{
VerdictHash = entry.VerdictHash,
LedgerId = entry.LedgerId,
CreatedAt = entry.CreatedAt
});
}
catch (Repositories.ChainIntegrityException ex)
{
return Results.Conflict(new { error = "Chain integrity violation", details = ex.Message });
}
}
private static async Task<IResult> QueryVerdicts(
string bomRef,
IVerdictLedgerService service,
HttpContext context,
CancellationToken ct)
{
var tenantId = context.Request.Headers.TryGetValue("X-Tenant-Id", out var tid)
? Guid.Parse(tid.FirstOrDefault() ?? Guid.Empty.ToString())
: Guid.Empty;
var entries = await service.GetChainAsync(tenantId, "", "", ct);
var filtered = entries.Where(e => e.BomRef == bomRef).ToList();
return Results.Ok(filtered.Select(MapToResponse).ToList());
}
private static async Task<IResult> GetVerdictByHash(
string hash,
IVerdictLedgerService service,
CancellationToken ct)
{
// Service doesn't have GetByHash - need to add or use repository directly
// For now, return not implemented
return Results.NotFound(new { error = "Verdict not found" });
}
private static async Task<IResult> VerifyChain(
IVerdictLedgerService service,
HttpContext context,
CancellationToken ct)
{
var tenantId = context.Request.Headers.TryGetValue("X-Tenant-Id", out var tid)
? Guid.Parse(tid.FirstOrDefault() ?? Guid.Empty.ToString())
: Guid.Empty;
var result = await service.VerifyChainIntegrityAsync(tenantId, ct);
return Results.Ok(result);
}
private static async Task<IResult> GetLatestVerdict(
string bomRef,
IVerdictLedgerService service,
HttpContext context,
CancellationToken ct)
{
var tenantId = context.Request.Headers.TryGetValue("X-Tenant-Id", out var tid)
? Guid.Parse(tid.FirstOrDefault() ?? Guid.Empty.ToString())
: Guid.Empty;
var entry = await service.GetLatestVerdictAsync(bomRef, tenantId, ct);
if (entry == null)
{
return Results.NotFound(new { error = "No verdict found for bom_ref" });
}
return Results.Ok(MapToResponse(entry));
}
private static VerdictResponse MapToResponse(VerdictLedgerEntry entry)
{
return new VerdictResponse
{
LedgerId = entry.LedgerId,
BomRef = entry.BomRef,
CycloneDxSerial = entry.CycloneDxSerial,
RekorUuid = entry.RekorUuid,
Decision = entry.Decision.ToString().ToLowerInvariant(),
Reason = entry.Reason,
PolicyBundleId = entry.PolicyBundleId,
PolicyBundleHash = entry.PolicyBundleHash,
VerifierImageDigest = entry.VerifierImageDigest,
SignerKeyId = entry.SignerKeyId,
PrevHash = entry.PrevHash,
VerdictHash = entry.VerdictHash,
CreatedAt = entry.CreatedAt
};
}
}
// Request/Response DTOs
/// <summary>
/// Request to create a verdict.
/// </summary>
public sealed record CreateVerdictRequest
{
/// <summary>Package URL or container digest.</summary>
public string BomRef { get; init; } = "";
/// <summary>CycloneDX serial number.</summary>
public string? CycloneDxSerial { get; init; }
/// <summary>Rekor log entry UUID.</summary>
public string? RekorUuid { get; init; }
/// <summary>Decision: approve, reject, unknown, pending.</summary>
public string Decision { get; init; } = "unknown";
/// <summary>Reason for decision.</summary>
public string? Reason { get; init; }
/// <summary>Policy bundle ID.</summary>
public string PolicyBundleId { get; init; } = "";
/// <summary>Policy bundle hash.</summary>
public string? PolicyBundleHash { get; init; }
/// <summary>Verifier image digest.</summary>
public string? VerifierImageDigest { get; init; }
/// <summary>Signer key ID.</summary>
public string? SignerKeyId { get; init; }
/// <summary>DSSE signature (base64).</summary>
public string? Signature { get; init; }
}
/// <summary>
/// Response after creating a verdict.
/// </summary>
public sealed record CreateVerdictResponse
{
/// <summary>Computed verdict hash.</summary>
public required string VerdictHash { get; init; }
/// <summary>Ledger entry ID.</summary>
public Guid LedgerId { get; init; }
/// <summary>Creation timestamp.</summary>
public DateTimeOffset CreatedAt { get; init; }
}
/// <summary>
/// Verdict response DTO.
/// </summary>
public sealed record VerdictResponse
{
public Guid LedgerId { get; init; }
public string BomRef { get; init; } = "";
public string? CycloneDxSerial { get; init; }
public string? RekorUuid { get; init; }
public string Decision { get; init; } = "unknown";
public string? Reason { get; init; }
public string PolicyBundleId { get; init; } = "";
public string PolicyBundleHash { get; init; } = "";
public string VerifierImageDigest { get; init; } = "";
public string SignerKeyId { get; init; } = "";
public string? PrevHash { get; init; }
public string VerdictHash { get; init; } = "";
public DateTimeOffset CreatedAt { get; init; }
}