feat: Add CVSS receipt management endpoints and related functionality
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

- Introduced new API endpoints for creating, retrieving, amending, and listing CVSS receipts.
- Updated IPolicyEngineClient interface to include methods for CVSS receipt operations.
- Implemented PolicyEngineClient to handle CVSS receipt requests.
- Enhanced Program.cs to map new CVSS receipt routes with appropriate authorization.
- Added necessary models and contracts for CVSS receipt requests and responses.
- Integrated Postgres document store for managing CVSS receipts and related data.
- Updated database schema with new migrations for source documents and payload storage.
- Refactored existing components to support new CVSS functionality.
This commit is contained in:
StellaOps Bot
2025-12-07 00:43:14 +02:00
parent 0de92144d2
commit 53889d85e7
67 changed files with 17207 additions and 16293 deletions

View File

@@ -0,0 +1,327 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Attestor.Envelope;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Receipts;
namespace StellaOps.Policy.Engine.Endpoints;
/// <summary>
/// Minimal API surface for CVSS v4.0 score receipts (create, read, amend, history).
/// </summary>
internal static class CvssReceiptEndpoints
{
public static IEndpointRouteBuilder MapCvssReceipts(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/cvss")
.RequireAuthorization()
.WithTags("CVSS Receipts");
group.MapPost("/receipts", CreateReceipt)
.WithName("CreateCvssReceipt")
.WithSummary("Create a CVSS v4.0 receipt with deterministic hashing and optional DSSE attestation.")
.Produces<CvssScoreReceipt>(StatusCodes.Status201Created)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.Produces<ProblemHttpResult>(StatusCodes.Status401Unauthorized);
group.MapGet("/receipts/{receiptId}", GetReceipt)
.WithName("GetCvssReceipt")
.WithSummary("Retrieve a CVSS v4.0 receipt by ID.")
.Produces<CvssScoreReceipt>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapPut("/receipts/{receiptId}/amend", AmendReceipt)
.WithName("AmendCvssReceipt")
.WithSummary("Append an amendment entry to a CVSS receipt history and optionally re-sign.")
.Produces<CvssScoreReceipt>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapGet("/receipts/{receiptId}/history", GetReceiptHistory)
.WithName("GetCvssReceiptHistory")
.WithSummary("Return the ordered amendment history for a CVSS receipt.")
.Produces<IReadOnlyList<ReceiptHistoryEntry>>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
group.MapGet("/policies", ListPolicies)
.WithName("ListCvssPolicies")
.WithSummary("List available CVSS policies configured on this host.")
.Produces<IReadOnlyList<CvssPolicy>>(StatusCodes.Status200OK);
return endpoints;
}
private static async Task<IResult> CreateReceipt(
HttpContext context,
[FromBody] CreateCvssReceiptRequest request,
IReceiptBuilder receiptBuilder,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRun);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
if (request.Policy is null || string.IsNullOrWhiteSpace(request.Policy.Hash))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Policy hash required",
Detail = "CvssPolicy with a deterministic hash must be supplied.",
Status = StatusCodes.Status400BadRequest
});
}
var tenantId = ResolveTenantId(context);
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Tenant required",
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
Status = StatusCodes.Status400BadRequest
});
}
var actor = ResolveActorId(context) ?? request.CreatedBy ?? "system";
var createdAt = request.CreatedAt ?? DateTimeOffset.UtcNow;
var createRequest = new CreateReceiptRequest
{
TenantId = tenantId,
VulnerabilityId = request.VulnerabilityId,
CreatedBy = actor,
CreatedAt = createdAt,
Policy = request.Policy,
BaseMetrics = request.BaseMetrics,
ThreatMetrics = request.ThreatMetrics,
EnvironmentalMetrics = request.EnvironmentalMetrics ?? request.Policy.DefaultEnvironmentalMetrics,
SupplementalMetrics = request.SupplementalMetrics,
Evidence = request.Evidence?.ToImmutableList() ?? ImmutableList<CvssEvidenceItem>.Empty,
SigningKey = request.SigningKey
};
try
{
var receipt = await receiptBuilder.CreateAsync(createRequest, cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/cvss/receipts/{receipt.ReceiptId}", receipt);
}
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Failed to create CVSS receipt",
Detail = ex.Message,
Status = StatusCodes.Status400BadRequest
});
}
}
private static async Task<IResult> GetReceipt(
HttpContext context,
[FromRoute] string receiptId,
IReceiptRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.FindingsRead);
if (scopeResult is not null)
{
return scopeResult;
}
var tenantId = ResolveTenantId(context);
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Tenant required",
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
Status = StatusCodes.Status400BadRequest
});
}
var receipt = await repository.GetAsync(tenantId, receiptId, cancellationToken).ConfigureAwait(false);
if (receipt is null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Receipt not found",
Detail = $"CVSS receipt '{receiptId}' was not found.",
Status = StatusCodes.Status404NotFound
});
}
return Results.Ok(receipt);
}
private static async Task<IResult> AmendReceipt(
HttpContext context,
[FromRoute] string receiptId,
[FromBody] AmendCvssReceiptRequest request,
IReceiptHistoryService historyService,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRun);
if (scopeResult is not null)
{
return scopeResult;
}
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required.",
Status = StatusCodes.Status400BadRequest
});
}
var tenantId = ResolveTenantId(context);
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Tenant required",
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
Status = StatusCodes.Status400BadRequest
});
}
var actor = ResolveActorId(context) ?? request.Actor ?? "system";
var amend = new AmendReceiptRequest
{
ReceiptId = receiptId,
TenantId = tenantId,
Actor = actor,
Field = request.Field,
PreviousValue = request.PreviousValue,
NewValue = request.NewValue,
Reason = request.Reason,
ReferenceUri = request.ReferenceUri,
SigningKey = request.SigningKey
};
try
{
var amended = await historyService.AmendAsync(amend, cancellationToken).ConfigureAwait(false);
return Results.Ok(amended);
}
catch (InvalidOperationException ex)
{
return Results.NotFound(new ProblemDetails
{
Title = "Receipt not found",
Detail = ex.Message,
Status = StatusCodes.Status404NotFound
});
}
catch (Exception ex) when (ex is ArgumentException)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Failed to amend receipt",
Detail = ex.Message,
Status = StatusCodes.Status400BadRequest
});
}
}
private static async Task<IResult> GetReceiptHistory(
HttpContext context,
[FromRoute] string receiptId,
IReceiptRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.FindingsRead);
if (scopeResult is not null)
{
return scopeResult;
}
var tenantId = ResolveTenantId(context);
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Tenant required",
Detail = "Specify tenant via X-Tenant-Id header or tenant_id claim.",
Status = StatusCodes.Status400BadRequest
});
}
var receipt = await repository.GetAsync(tenantId, receiptId, cancellationToken).ConfigureAwait(false);
if (receipt is null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Receipt not found",
Detail = $"CVSS receipt '{receiptId}' was not found.",
Status = StatusCodes.Status404NotFound
});
}
var orderedHistory = receipt.History
.OrderBy(h => h.Timestamp)
.ToList();
return Results.Ok(orderedHistory);
}
private static IResult ListPolicies()
=> Results.Ok(Array.Empty<CvssPolicy>());
private static string? ResolveTenantId(HttpContext context)
{
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantHeader) &&
!string.IsNullOrWhiteSpace(tenantHeader))
{
return tenantHeader.ToString();
}
return context.User?.FindFirst("tenant_id")?.Value;
}
private static string? ResolveActorId(HttpContext context)
{
var user = context.User;
return user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
?? user?.FindFirst("sub")?.Value;
}
}
internal sealed record CreateCvssReceiptRequest(
string VulnerabilityId,
CvssPolicy Policy,
CvssBaseMetrics BaseMetrics,
CvssThreatMetrics? ThreatMetrics,
CvssEnvironmentalMetrics? EnvironmentalMetrics,
CvssSupplementalMetrics? SupplementalMetrics,
IReadOnlyList<CvssEvidenceItem>? Evidence,
EnvelopeKey? SigningKey,
string? CreatedBy,
DateTimeOffset? CreatedAt);
internal sealed record AmendCvssReceiptRequest(
string Field,
string? PreviousValue,
string? NewValue,
string Reason,
string? ReferenceUri,
EnvelopeKey? SigningKey,
string? Actor);