using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using StellaOps.EvidenceLocker.Storage;
namespace StellaOps.EvidenceLocker.Api;
///
/// Logging category for verdict endpoints.
///
internal sealed class VerdictEndpointsLogger;
///
/// Minimal API endpoints for verdict attestations.
///
public static class VerdictEndpoints
{
public static void MapVerdictEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/v1/verdicts")
.WithTags("Verdicts");
// POST /api/v1/verdicts
group.MapPost("/", StoreVerdictAsync)
.WithName("StoreVerdict")
.WithSummary("Store a verdict attestation")
.Produces(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status500InternalServerError);
// GET /api/v1/verdicts/{verdictId}
group.MapGet("/{verdictId}", GetVerdictAsync)
.WithName("GetVerdict")
.WithSummary("Retrieve a verdict attestation by ID")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
// GET /api/v1/runs/{runId}/verdicts
app.MapGet("/api/v1/runs/{runId}/verdicts", ListVerdictsForRunAsync)
.WithName("ListVerdictsForRun")
.WithTags("Verdicts")
.WithSummary("List verdict attestations for a policy run")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
// POST /api/v1/verdicts/{verdictId}/verify
group.MapPost("/{verdictId}/verify", VerifyVerdictAsync)
.WithName("VerifyVerdict")
.WithSummary("Verify verdict attestation signature")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
// GET /api/v1/verdicts/{verdictId}/envelope - SPRINT_4000_0100_0001
group.MapGet("/{verdictId}/envelope", DownloadEnvelopeAsync)
.WithName("DownloadEnvelope")
.WithSummary("Download DSSE envelope for verdict")
.Produces(StatusCodes.Status200OK, contentType: "application/json")
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
}
private static async Task StoreVerdictAsync(
[FromBody] StoreVerdictRequest request,
[FromServices] IVerdictRepository repository,
[FromServices] TimeProvider timeProvider,
[FromServices] ILogger logger,
CancellationToken cancellationToken)
{
try
{
logger.LogInformation("Storing verdict attestation {VerdictId}", request.VerdictId);
// Validate request
if (string.IsNullOrWhiteSpace(request.VerdictId))
{
return Results.BadRequest(new { error = "verdict_id is required" });
}
if (string.IsNullOrWhiteSpace(request.FindingId))
{
return Results.BadRequest(new { error = "finding_id is required" });
}
// Serialize envelope to JSON string
var envelopeJson = JsonSerializer.Serialize(request.Envelope);
// Create repository record
var record = new VerdictAttestationRecord
{
VerdictId = request.VerdictId,
TenantId = request.TenantId,
RunId = request.PolicyRunId,
PolicyId = request.PolicyId,
PolicyVersion = request.PolicyVersion,
FindingId = request.FindingId,
VerdictStatus = request.VerdictStatus,
VerdictSeverity = request.VerdictSeverity,
VerdictScore = request.VerdictScore,
EvaluatedAt = request.EvaluatedAt,
Envelope = envelopeJson,
PredicateDigest = request.PredicateDigest,
DeterminismHash = request.DeterminismHash,
RekorLogIndex = request.RekorLogIndex,
CreatedAt = timeProvider.GetUtcNow()
};
// Store in repository
var storedVerdictId = await repository.StoreVerdictAsync(record, cancellationToken);
logger.LogInformation("Successfully stored verdict attestation {VerdictId}", storedVerdictId);
var response = new StoreVerdictResponse
{
VerdictId = storedVerdictId,
CreatedAt = record.CreatedAt,
Stored = true
};
return Results.Created($"/api/v1/verdicts/{storedVerdictId}", response);
}
catch (Exception ex)
{
logger.LogError(ex, "Error storing verdict attestation {VerdictId}", request.VerdictId);
return Results.Problem(
title: "Internal server error",
detail: "Failed to store verdict attestation",
statusCode: StatusCodes.Status500InternalServerError
);
}
}
private static async Task GetVerdictAsync(
string verdictId,
[FromServices] IVerdictRepository repository,
[FromServices] ILogger logger,
CancellationToken cancellationToken)
{
try
{
logger.LogInformation("Retrieving verdict attestation {VerdictId}", verdictId);
var record = await repository.GetVerdictAsync(verdictId, cancellationToken);
if (record is null)
{
logger.LogWarning("Verdict attestation {VerdictId} not found", verdictId);
return Results.NotFound(new { error = "Verdict not found", verdict_id = verdictId });
}
// Parse envelope JSON
var envelope = JsonSerializer.Deserialize