Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -0,0 +1,330 @@
using Microsoft.AspNetCore.Mvc;
using StellaOps.VexLens.Api;
namespace StellaOps.VexLens.WebService.Extensions;
/// <summary>
/// Extension methods for mapping VexLens API endpoints.
/// </summary>
public static class VexLensEndpointExtensions
{
private const string TenantHeader = "X-StellaOps-Tenant";
/// <summary>
/// Maps all VexLens API endpoints.
/// </summary>
public static IEndpointRouteBuilder MapVexLensEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/vexlens")
.WithTags("VexLens")
.WithOpenApi();
// Consensus endpoints
group.MapPost("/consensus", ComputeConsensusAsync)
.WithName("ComputeConsensus")
.WithDescription("Compute consensus for a vulnerability-product pair")
.Produces<ComputeConsensusResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest);
group.MapPost("/consensus:batch", ComputeConsensusBatchAsync)
.WithName("ComputeConsensusBatch")
.WithDescription("Compute consensus for multiple vulnerability-product pairs")
.Produces<ComputeConsensusBatchResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest);
// Projection endpoints
group.MapGet("/projections", QueryProjectionsAsync)
.WithName("QueryProjections")
.WithDescription("Query consensus projections with filtering")
.Produces<QueryProjectionsResponse>(StatusCodes.Status200OK);
group.MapGet("/projections/{projectionId}", GetProjectionAsync)
.WithName("GetProjection")
.WithDescription("Get a specific consensus projection by ID")
.Produces<ProjectionDetailResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/projections/latest", GetLatestProjectionAsync)
.WithName("GetLatestProjection")
.WithDescription("Get the latest projection for a vulnerability-product pair")
.Produces<ProjectionDetailResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/projections/history", GetProjectionHistoryAsync)
.WithName("GetProjectionHistory")
.WithDescription("Get projection history for a vulnerability-product pair")
.Produces<ProjectionHistoryResponse>(StatusCodes.Status200OK);
// Statistics endpoint
group.MapGet("/stats", GetStatisticsAsync)
.WithName("GetVexLensStatistics")
.WithDescription("Get consensus projection statistics")
.Produces<ConsensusStatisticsResponse>(StatusCodes.Status200OK);
// Conflict endpoints
group.MapGet("/conflicts", GetConflictsAsync)
.WithName("GetConflicts")
.WithDescription("Get projections with conflicts")
.Produces<QueryProjectionsResponse>(StatusCodes.Status200OK);
// Issuer endpoints
var issuerGroup = app.MapGroup("/api/v1/vexlens/issuers")
.WithTags("VexLens Issuers")
.WithOpenApi();
issuerGroup.MapGet("/", ListIssuersAsync)
.WithName("ListIssuers")
.WithDescription("List registered VEX issuers")
.Produces<IssuerListResponse>(StatusCodes.Status200OK);
issuerGroup.MapGet("/{issuerId}", GetIssuerAsync)
.WithName("GetIssuer")
.WithDescription("Get issuer details")
.Produces<IssuerDetailResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
issuerGroup.MapPost("/", RegisterIssuerAsync)
.WithName("RegisterIssuer")
.WithDescription("Register a new VEX issuer")
.Produces<IssuerDetailResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest);
issuerGroup.MapDelete("/{issuerId}", RevokeIssuerAsync)
.WithName("RevokeIssuer")
.WithDescription("Revoke an issuer")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound);
issuerGroup.MapPost("/{issuerId}/keys", AddIssuerKeyAsync)
.WithName("AddIssuerKey")
.WithDescription("Add a key to an issuer")
.Produces<IssuerDetailResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
issuerGroup.MapDelete("/{issuerId}/keys/{fingerprint}", RevokeIssuerKeyAsync)
.WithName("RevokeIssuerKey")
.WithDescription("Revoke an issuer key")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound);
return app;
}
private static string? GetTenantId(HttpContext context)
{
return context.Request.Headers.TryGetValue(TenantHeader, out var value)
? value.ToString()
: null;
}
// Consensus handlers
private static async Task<IResult> ComputeConsensusAsync(
[FromBody] ComputeConsensusRequest request,
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context) ?? request.TenantId;
var requestWithTenant = request with { TenantId = tenantId };
var result = await service.ComputeConsensusAsync(requestWithTenant, cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> ComputeConsensusBatchAsync(
[FromBody] ComputeConsensusBatchRequest request,
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context) ?? request.TenantId;
var requestWithTenant = request with { TenantId = tenantId };
var result = await service.ComputeConsensusBatchAsync(requestWithTenant, cancellationToken);
return Results.Ok(result);
}
// Projection handlers
private static async Task<IResult> QueryProjectionsAsync(
[AsParameters] ProjectionQueryParams query,
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context);
var request = new QueryProjectionsRequest(
VulnerabilityId: query.VulnerabilityId,
ProductKey: query.ProductKey,
Status: null,
Outcome: query.Outcome,
MinimumConfidence: query.MinimumConfidence,
ComputedAfter: query.ComputedAfter,
ComputedBefore: query.ComputedBefore,
StatusChanged: query.StatusChanged,
Limit: query.Limit,
Offset: query.Offset,
SortBy: query.SortBy,
SortDescending: query.SortDescending);
var result = await service.QueryProjectionsAsync(request, tenantId, cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetProjectionAsync(
string projectionId,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var result = await service.GetProjectionAsync(projectionId, cancellationToken);
return result != null ? Results.Ok(result) : Results.NotFound();
}
private static async Task<IResult> GetLatestProjectionAsync(
[FromQuery] string vulnerabilityId,
[FromQuery] string productKey,
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context);
var result = await service.GetLatestProjectionAsync(vulnerabilityId, productKey, tenantId, cancellationToken);
return result != null ? Results.Ok(result) : Results.NotFound();
}
private static async Task<IResult> GetProjectionHistoryAsync(
[FromQuery] string vulnerabilityId,
[FromQuery] string productKey,
[FromQuery] int? limit,
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context);
var result = await service.GetProjectionHistoryAsync(vulnerabilityId, productKey, tenantId, limit, cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetStatisticsAsync(
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context);
var result = await service.GetStatisticsAsync(tenantId, cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetConflictsAsync(
[FromQuery] int? limit,
[FromQuery] int? offset,
[FromServices] IVexLensApiService service,
HttpContext context,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context);
// Query for projections with conflicts (conflictCount > 0)
var request = new QueryProjectionsRequest(
VulnerabilityId: null,
ProductKey: null,
Status: null,
Outcome: null,
MinimumConfidence: null,
ComputedAfter: null,
ComputedBefore: null,
StatusChanged: null,
Limit: limit ?? 50,
Offset: offset ?? 0,
SortBy: "ComputedAt",
SortDescending: true);
var result = await service.QueryProjectionsAsync(request, tenantId, cancellationToken);
// Filter to only show projections with conflicts
var conflictsOnly = new QueryProjectionsResponse(
Projections: result.Projections.Where(p => p.ConflictCount > 0).ToList(),
TotalCount: result.Projections.Count(p => p.ConflictCount > 0),
Offset: result.Offset,
Limit: result.Limit);
return Results.Ok(conflictsOnly);
}
// Issuer handlers
private static async Task<IResult> ListIssuersAsync(
[FromQuery] string? category,
[FromQuery] string? minimumTrustTier,
[FromQuery] string? status,
[FromQuery] string? search,
[FromQuery] int? limit,
[FromQuery] int? offset,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var result = await service.ListIssuersAsync(
category, minimumTrustTier, status, search, limit, offset, cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetIssuerAsync(
string issuerId,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var result = await service.GetIssuerAsync(issuerId, cancellationToken);
return result != null ? Results.Ok(result) : Results.NotFound();
}
private static async Task<IResult> RegisterIssuerAsync(
[FromBody] RegisterIssuerRequest request,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var result = await service.RegisterIssuerAsync(request, cancellationToken);
return Results.Created($"/api/v1/vexlens/issuers/{result.IssuerId}", result);
}
private static async Task<IResult> RevokeIssuerAsync(
string issuerId,
[FromBody] RevokeRequest request,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var success = await service.RevokeIssuerAsync(issuerId, request, cancellationToken);
return success ? Results.NoContent() : Results.NotFound();
}
private static async Task<IResult> AddIssuerKeyAsync(
string issuerId,
[FromBody] RegisterKeyRequest request,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var result = await service.AddIssuerKeyAsync(issuerId, request, cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> RevokeIssuerKeyAsync(
string issuerId,
string fingerprint,
[FromBody] RevokeRequest request,
[FromServices] IVexLensApiService service,
CancellationToken cancellationToken)
{
var success = await service.RevokeIssuerKeyAsync(issuerId, fingerprint, request, cancellationToken);
return success ? Results.NoContent() : Results.NotFound();
}
}
/// <summary>
/// Query parameters for projection queries.
/// </summary>
public sealed record ProjectionQueryParams(
[FromQuery] string? VulnerabilityId,
[FromQuery] string? ProductKey,
[FromQuery] string? Outcome,
[FromQuery] double? MinimumConfidence,
[FromQuery] DateTimeOffset? ComputedAfter,
[FromQuery] DateTimeOffset? ComputedBefore,
[FromQuery] bool? StatusChanged,
[FromQuery] int? Limit,
[FromQuery] int? Offset,
[FromQuery] string? SortBy,
[FromQuery] bool? SortDescending);