new two advisories and sprints work on them

This commit is contained in:
master
2026-01-16 18:39:36 +02:00
parent 9daf619954
commit c3a6269d55
72 changed files with 15540 additions and 18 deletions

View File

@@ -0,0 +1,312 @@
// -----------------------------------------------------------------------------
// RekorAttestationEndpoints.cs
// Sprint: SPRINT_20260117_002_EXCITITOR_vex_rekor_linkage
// Task: VRL-007 - Create API endpoints for VEX-Rekor attestation management
// Description: REST API endpoints for VEX observation attestation to Rekor
// -----------------------------------------------------------------------------
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core.Observations;
using StellaOps.Excititor.Core.Storage;
using static Program;
namespace StellaOps.Excititor.WebService.Endpoints;
/// <summary>
/// API endpoints for managing VEX observation attestation to Rekor transparency log.
/// </summary>
public static class RekorAttestationEndpoints
{
public static void MapRekorAttestationEndpoints(this WebApplication app)
{
var group = app.MapGroup("/attestations/rekor")
.WithTags("Rekor Attestation");
// POST /attestations/rekor/observations/{observationId}
// Attest a single observation to Rekor
group.MapPost("/observations/{observationId}", async (
HttpContext context,
string observationId,
[FromBody] AttestObservationRequest? request,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexObservationAttestationService? attestationService,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.attest");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
if (attestationService is null)
{
return Results.Problem(
detail: "Attestation service is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}
if (string.IsNullOrWhiteSpace(observationId))
{
return Results.Problem(
detail: "observationId is required.",
statusCode: StatusCodes.Status400BadRequest,
title: "Validation error");
}
var options = new VexAttestationOptions
{
SubmitToRekor = true,
RekorUrl = request?.RekorUrl,
StoreInclusionProof = request?.StoreInclusionProof ?? true,
SigningKeyId = request?.SigningKeyId,
TraceId = context.TraceIdentifier
};
// Get observation and attest it
// Note: In real implementation, we'd fetch the observation first
var result = await attestationService.AttestAndLinkAsync(
new VexObservation { Id = observationId },
options,
cancellationToken);
if (!result.Success)
{
return Results.Problem(
detail: result.ErrorMessage,
statusCode: result.ErrorCode switch
{
VexAttestationErrorCode.ObservationNotFound => StatusCodes.Status404NotFound,
VexAttestationErrorCode.AlreadyAttested => StatusCodes.Status409Conflict,
VexAttestationErrorCode.Timeout => StatusCodes.Status504GatewayTimeout,
_ => StatusCodes.Status500InternalServerError
},
title: "Attestation failed");
}
var response = new AttestObservationResponse(
observationId,
result.RekorLinkage!.EntryUuid,
result.RekorLinkage.LogIndex,
result.RekorLinkage.IntegratedTime,
result.Duration);
return Results.Ok(response);
}).WithName("AttestObservationToRekor");
// POST /attestations/rekor/observations/batch
// Attest multiple observations to Rekor
group.MapPost("/observations/batch", async (
HttpContext context,
[FromBody] BatchAttestRequest request,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexObservationAttestationService? attestationService,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.attest");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
if (attestationService is null)
{
return Results.Problem(
detail: "Attestation service is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}
if (request.ObservationIds is null || request.ObservationIds.Count == 0)
{
return Results.Problem(
detail: "observationIds is required and must not be empty.",
statusCode: StatusCodes.Status400BadRequest,
title: "Validation error");
}
if (request.ObservationIds.Count > 100)
{
return Results.Problem(
detail: "Maximum 100 observations per batch.",
statusCode: StatusCodes.Status400BadRequest,
title: "Validation error");
}
var options = new VexAttestationOptions
{
SubmitToRekor = true,
RekorUrl = request.RekorUrl,
StoreInclusionProof = request.StoreInclusionProof ?? true,
SigningKeyId = request.SigningKeyId,
TraceId = context.TraceIdentifier
};
var results = await attestationService.AttestBatchAsync(
request.ObservationIds,
options,
cancellationToken);
var items = results.Select(r => new BatchAttestResultItem(
r.ObservationId,
r.Success,
r.RekorLinkage?.EntryUuid,
r.RekorLinkage?.LogIndex,
r.ErrorMessage,
r.ErrorCode?.ToString()
)).ToList();
var response = new BatchAttestResponse(
items.Count(i => i.Success),
items.Count(i => !i.Success),
items);
return Results.Ok(response);
}).WithName("BatchAttestObservationsToRekor");
// GET /attestations/rekor/observations/{observationId}/verify
// Verify an observation's Rekor linkage
group.MapGet("/observations/{observationId}/verify", async (
HttpContext context,
string observationId,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexObservationAttestationService? attestationService,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
if (attestationService is null)
{
return Results.Problem(
detail: "Attestation service is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}
if (string.IsNullOrWhiteSpace(observationId))
{
return Results.Problem(
detail: "observationId is required.",
statusCode: StatusCodes.Status400BadRequest,
title: "Validation error");
}
var result = await attestationService.VerifyLinkageAsync(observationId, cancellationToken);
var response = new VerifyLinkageResponse(
observationId,
result.IsVerified,
result.VerifiedAt,
result.RekorEntryId,
result.LogIndex,
result.FailureReason);
return Results.Ok(response);
}).WithName("VerifyObservationRekorLinkage");
// GET /attestations/rekor/pending
// Get observations pending attestation
group.MapGet("/pending", async (
HttpContext context,
[FromQuery] int? limit,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexObservationAttestationService? attestationService,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
{
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
if (attestationService is null)
{
return Results.Problem(
detail: "Attestation service is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}
var pendingIds = await attestationService.GetPendingAttestationsAsync(
limit ?? 100,
cancellationToken);
var response = new PendingAttestationsResponse(pendingIds.Count, pendingIds);
return Results.Ok(response);
}).WithName("GetPendingRekorAttestations");
}
}
// Request DTOs
public sealed record AttestObservationRequest(
[property: JsonPropertyName("rekorUrl")] string? RekorUrl,
[property: JsonPropertyName("storeInclusionProof")] bool? StoreInclusionProof,
[property: JsonPropertyName("signingKeyId")] string? SigningKeyId);
public sealed record BatchAttestRequest(
[property: JsonPropertyName("observationIds")] IReadOnlyList<string> ObservationIds,
[property: JsonPropertyName("rekorUrl")] string? RekorUrl,
[property: JsonPropertyName("storeInclusionProof")] bool? StoreInclusionProof,
[property: JsonPropertyName("signingKeyId")] string? SigningKeyId);
// Response DTOs
public sealed record AttestObservationResponse(
[property: JsonPropertyName("observationId")] string ObservationId,
[property: JsonPropertyName("rekorEntryId")] string RekorEntryId,
[property: JsonPropertyName("logIndex")] long LogIndex,
[property: JsonPropertyName("integratedTime")] DateTimeOffset IntegratedTime,
[property: JsonPropertyName("duration")] TimeSpan? Duration);
public sealed record BatchAttestResultItem(
[property: JsonPropertyName("observationId")] string ObservationId,
[property: JsonPropertyName("success")] bool Success,
[property: JsonPropertyName("rekorEntryId")] string? RekorEntryId,
[property: JsonPropertyName("logIndex")] long? LogIndex,
[property: JsonPropertyName("error")] string? Error,
[property: JsonPropertyName("errorCode")] string? ErrorCode);
public sealed record BatchAttestResponse(
[property: JsonPropertyName("successCount")] int SuccessCount,
[property: JsonPropertyName("failureCount")] int FailureCount,
[property: JsonPropertyName("results")] IReadOnlyList<BatchAttestResultItem> Results);
public sealed record VerifyLinkageResponse(
[property: JsonPropertyName("observationId")] string ObservationId,
[property: JsonPropertyName("isVerified")] bool IsVerified,
[property: JsonPropertyName("verifiedAt")] DateTimeOffset? VerifiedAt,
[property: JsonPropertyName("rekorEntryId")] string? RekorEntryId,
[property: JsonPropertyName("logIndex")] long? LogIndex,
[property: JsonPropertyName("failureReason")] string? FailureReason);
public sealed record PendingAttestationsResponse(
[property: JsonPropertyName("count")] int Count,
[property: JsonPropertyName("observationIds")] IReadOnlyList<string> ObservationIds);