audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -0,0 +1,233 @@
// -----------------------------------------------------------------------------
// ExportEndpoints.cs
// Sprint: SPRINT_20260106_003_003_EVIDENCE_export_bundle
// Tasks: T020, T021
// Description: Minimal API endpoints for evidence bundle export.
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace StellaOps.EvidenceLocker.Api;
/// <summary>
/// Minimal API endpoints for evidence bundle export.
/// </summary>
public static class ExportEndpoints
{
/// <summary>
/// Maps export endpoints to the application.
/// </summary>
public static void MapExportEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/v1/bundles")
.WithTags("Export");
// POST /api/v1/bundles/{bundleId}/export
group.MapPost("/{bundleId}/export", TriggerExportAsync)
.WithName("TriggerExport")
.WithSummary("Trigger an async evidence bundle export")
.Produces<ExportTriggerResponse>(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization();
// GET /api/v1/bundles/{bundleId}/export/{exportId}
group.MapGet("/{bundleId}/export/{exportId}", GetExportStatusAsync)
.WithName("GetExportStatus")
.WithSummary("Get export status or download exported bundle")
.Produces<ExportStatusResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization();
// GET /api/v1/bundles/{bundleId}/export/{exportId}/download
group.MapGet("/{bundleId}/export/{exportId}/download", DownloadExportAsync)
.WithName("DownloadExport")
.WithSummary("Download the exported bundle")
.Produces(StatusCodes.Status200OK, contentType: "application/gzip")
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict)
.RequireAuthorization();
}
private static async Task<IResult> TriggerExportAsync(
string bundleId,
[FromBody] ExportTriggerRequest request,
[FromServices] IExportJobService exportJobService,
CancellationToken cancellationToken)
{
var result = await exportJobService.CreateExportJobAsync(bundleId, request, cancellationToken);
if (result.IsNotFound)
{
return Results.NotFound(new { message = $"Bundle '{bundleId}' not found" });
}
return Results.Accepted(
$"/api/v1/bundles/{bundleId}/export/{result.ExportId}",
new ExportTriggerResponse
{
ExportId = result.ExportId,
Status = result.Status,
EstimatedSize = result.EstimatedSize,
StatusUrl = $"/api/v1/bundles/{bundleId}/export/{result.ExportId}"
});
}
private static async Task<IResult> GetExportStatusAsync(
string bundleId,
string exportId,
[FromServices] IExportJobService exportJobService,
CancellationToken cancellationToken)
{
var result = await exportJobService.GetExportStatusAsync(bundleId, exportId, cancellationToken);
if (result is null)
{
return Results.NotFound(new { message = $"Export '{exportId}' not found" });
}
if (result.Status == ExportJobStatusEnum.Ready)
{
return Results.Ok(new ExportStatusResponse
{
ExportId = result.ExportId,
Status = result.Status.ToString().ToLowerInvariant(),
DownloadUrl = $"/api/v1/bundles/{bundleId}/export/{exportId}/download",
FileSize = result.FileSize,
CompletedAt = result.CompletedAt
});
}
return Results.Accepted(value: new ExportStatusResponse
{
ExportId = result.ExportId,
Status = result.Status.ToString().ToLowerInvariant(),
Progress = result.Progress,
EstimatedTimeRemaining = result.EstimatedTimeRemaining
});
}
private static async Task<IResult> DownloadExportAsync(
string bundleId,
string exportId,
[FromServices] IExportJobService exportJobService,
CancellationToken cancellationToken)
{
var result = await exportJobService.GetExportFileAsync(bundleId, exportId, cancellationToken);
if (result is null)
{
return Results.NotFound(new { message = $"Export '{exportId}' not found" });
}
if (result.Status != ExportJobStatusEnum.Ready)
{
return Results.Conflict(new { message = "Export is not ready for download", status = result.Status.ToString().ToLowerInvariant() });
}
return Results.File(
result.FileStream!,
contentType: "application/gzip",
fileDownloadName: result.FileName);
}
}
/// <summary>
/// Request to trigger an evidence bundle export.
/// </summary>
public sealed record ExportTriggerRequest
{
/// <summary>
/// Export format (default: tar.gz).
/// </summary>
public string Format { get; init; } = "tar.gz";
/// <summary>
/// Compression type (gzip, brotli).
/// </summary>
public string Compression { get; init; } = "gzip";
/// <summary>
/// Compression level (1-9).
/// </summary>
public int CompressionLevel { get; init; } = 6;
/// <summary>
/// Whether to include Rekor transparency proofs.
/// </summary>
public bool IncludeRekorProofs { get; init; } = true;
/// <summary>
/// Whether to include per-layer SBOMs.
/// </summary>
public bool IncludeLayerSboms { get; init; } = true;
}
/// <summary>
/// Response after triggering an export.
/// </summary>
public sealed record ExportTriggerResponse
{
/// <summary>
/// Unique export job ID.
/// </summary>
public required string ExportId { get; init; }
/// <summary>
/// Current export status.
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Estimated final bundle size in bytes.
/// </summary>
public long? EstimatedSize { get; init; }
/// <summary>
/// URL to poll for status updates.
/// </summary>
public required string StatusUrl { get; init; }
}
/// <summary>
/// Response for export status queries.
/// </summary>
public sealed record ExportStatusResponse
{
/// <summary>
/// Export job ID.
/// </summary>
public required string ExportId { get; init; }
/// <summary>
/// Current status: pending, processing, ready, failed.
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Progress percentage (0-100) when processing.
/// </summary>
public int? Progress { get; init; }
/// <summary>
/// Estimated time remaining when processing.
/// </summary>
public string? EstimatedTimeRemaining { get; init; }
/// <summary>
/// Download URL when ready.
/// </summary>
public string? DownloadUrl { get; init; }
/// <summary>
/// Final file size in bytes when ready.
/// </summary>
public long? FileSize { get; init; }
/// <summary>
/// Completion timestamp when ready.
/// </summary>
public DateTimeOffset? CompletedAt { get; init; }
}