up
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.ExportCenter.Client.Models;
|
||||
|
||||
namespace StellaOps.ExportCenter.WebService.AuditBundle;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for mapping audit bundle endpoints.
|
||||
/// </summary>
|
||||
public static class AuditBundleEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps audit bundle endpoints to the application.
|
||||
/// </summary>
|
||||
public static WebApplication MapAuditBundleEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/v1/audit-bundles")
|
||||
.WithTags("Audit Bundles")
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportOperator);
|
||||
|
||||
// POST /v1/audit-bundles - Create a new audit bundle
|
||||
group.MapPost("", CreateAuditBundleAsync)
|
||||
.WithName("CreateAuditBundle")
|
||||
.WithSummary("Create an immutable audit bundle")
|
||||
.WithDescription("Creates a new audit bundle containing vulnerability reports, VEX decisions, policy evaluations, and attestations for a subject artifact.")
|
||||
.Produces<CreateAuditBundleResponse>(StatusCodes.Status202Accepted)
|
||||
.Produces<ErrorEnvelope>(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /v1/audit-bundles - List audit bundles
|
||||
group.MapGet("", ListAuditBundlesAsync)
|
||||
.WithName("ListAuditBundles")
|
||||
.WithSummary("List audit bundles")
|
||||
.WithDescription("Returns audit bundles, optionally filtered by subject or status.")
|
||||
.Produces<AuditBundleListResponse>(StatusCodes.Status200OK);
|
||||
|
||||
// GET /v1/audit-bundles/{bundleId} - Get audit bundle status
|
||||
group.MapGet("/{bundleId}", GetAuditBundleAsync)
|
||||
.WithName("GetAuditBundle")
|
||||
.WithSummary("Get audit bundle status")
|
||||
.WithDescription("Returns the status and details of a specific audit bundle.")
|
||||
.Produces<AuditBundleStatus>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// GET /v1/audit-bundles/{bundleId}/download - Download audit bundle
|
||||
group.MapGet("/{bundleId}/download", DownloadAuditBundleAsync)
|
||||
.WithName("DownloadAuditBundle")
|
||||
.WithSummary("Download audit bundle")
|
||||
.WithDescription("Downloads the completed audit bundle as a ZIP file with integrity verification.")
|
||||
.Produces(StatusCodes.Status200OK, contentType: "application/zip")
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status409Conflict);
|
||||
|
||||
// GET /v1/audit-bundles/{bundleId}/index - Get audit bundle index
|
||||
group.MapGet("/{bundleId}/index", GetAuditBundleIndexAsync)
|
||||
.WithName("GetAuditBundleIndex")
|
||||
.WithSummary("Get audit bundle index")
|
||||
.WithDescription("Returns the index manifest of a completed audit bundle.")
|
||||
.Produces<AuditBundleIndexDto>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<Results<Accepted<CreateAuditBundleResponse>, BadRequest<ErrorEnvelope>>> CreateAuditBundleAsync(
|
||||
[FromBody] CreateAuditBundleRequest request,
|
||||
[FromServices] IAuditBundleJobHandler handler,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get actor from claims
|
||||
var actorId = httpContext.User.FindFirst("sub")?.Value
|
||||
?? httpContext.User.FindFirst("preferred_username")?.Value
|
||||
?? "anonymous";
|
||||
var actorName = httpContext.User.FindFirst("name")?.Value
|
||||
?? httpContext.User.FindFirst("preferred_username")?.Value
|
||||
?? "Anonymous User";
|
||||
|
||||
var result = await handler.CreateBundleAsync(request, actorId, actorName, cancellationToken);
|
||||
|
||||
if (result.Error is not null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ErrorEnvelope(result.Error));
|
||||
}
|
||||
|
||||
return TypedResults.Accepted($"/v1/audit-bundles/{result.Response!.BundleId}", result.Response);
|
||||
}
|
||||
|
||||
private static async Task<Ok<AuditBundleListResponse>> ListAuditBundlesAsync(
|
||||
[FromQuery] string? subjectName,
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? continuationToken,
|
||||
[FromServices] IAuditBundleJobHandler handler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await handler.ListBundlesAsync(subjectName, status, limit ?? 50, continuationToken, cancellationToken);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<AuditBundleStatus>, NotFound>> GetAuditBundleAsync(
|
||||
string bundleId,
|
||||
[FromServices] IAuditBundleJobHandler handler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var status = await handler.GetBundleStatusAsync(bundleId, cancellationToken);
|
||||
if (status is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
return TypedResults.Ok(status);
|
||||
}
|
||||
|
||||
private static async Task<Results<FileContentHttpResult, NotFound, Conflict<string>>> DownloadAuditBundleAsync(
|
||||
string bundleId,
|
||||
[FromServices] IAuditBundleJobHandler handler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var status = await handler.GetBundleStatusAsync(bundleId, cancellationToken);
|
||||
if (status is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
if (status.Status != "Completed")
|
||||
{
|
||||
return TypedResults.Conflict($"Bundle is not ready for download. Current status: {status.Status}");
|
||||
}
|
||||
|
||||
var content = await handler.GetBundleContentAsync(bundleId, cancellationToken);
|
||||
if (content is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.File(content, "application/zip", $"audit-bundle-{bundleId}.zip");
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<AuditBundleIndexDto>, NotFound>> GetAuditBundleIndexAsync(
|
||||
string bundleId,
|
||||
[FromServices] IAuditBundleJobHandler handler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var index = await handler.GetBundleIndexAsync(bundleId, cancellationToken);
|
||||
if (index is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
return TypedResults.Ok(index);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.ExportCenter.Client.Models;
|
||||
|
||||
namespace StellaOps.ExportCenter.WebService.AuditBundle;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of audit bundle job handler for development/testing.
|
||||
/// Production would use PostgreSQL repository and background job processing.
|
||||
/// </summary>
|
||||
public sealed class AuditBundleJobHandler : IAuditBundleJobHandler
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, AuditBundleJob> _jobs = new();
|
||||
private readonly ILogger<AuditBundleJobHandler> _logger;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public AuditBundleJobHandler(ILogger<AuditBundleJobHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<AuditBundleCreateResult> CreateBundleAsync(
|
||||
CreateAuditBundleRequest request,
|
||||
string actorId,
|
||||
string actorName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Validate request
|
||||
if (request.Subject is null)
|
||||
{
|
||||
return Task.FromResult(new AuditBundleCreateResult(
|
||||
null,
|
||||
new ErrorDetail("INVALID_REQUEST", "Subject is required")));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Subject.Name))
|
||||
{
|
||||
return Task.FromResult(new AuditBundleCreateResult(
|
||||
null,
|
||||
new ErrorDetail("INVALID_REQUEST", "Subject name is required")));
|
||||
}
|
||||
|
||||
var bundleId = $"bndl-{Guid.NewGuid():N}";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var job = new AuditBundleJob
|
||||
{
|
||||
BundleId = bundleId,
|
||||
Status = "Pending",
|
||||
Progress = 0,
|
||||
CreatedAt = now,
|
||||
CreatedBy = new BundleActorRefDto(actorId, actorName),
|
||||
Subject = request.Subject,
|
||||
TimeWindow = request.TimeWindow,
|
||||
IncludeContent = request.IncludeContent
|
||||
};
|
||||
|
||||
_jobs[bundleId] = job;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created audit bundle job {BundleId} for subject {SubjectName}",
|
||||
bundleId, request.Subject.Name);
|
||||
|
||||
// In a real implementation, this would enqueue a background job
|
||||
// For now, we'll process it synchronously in-memory
|
||||
_ = Task.Run(async () => await ProcessBundleAsync(bundleId, cancellationToken), cancellationToken);
|
||||
|
||||
var response = new CreateAuditBundleResponse(
|
||||
bundleId,
|
||||
"Pending",
|
||||
$"/v1/audit-bundles/{bundleId}",
|
||||
EstimatedCompletionSeconds: 30);
|
||||
|
||||
return Task.FromResult(new AuditBundleCreateResult(response, null));
|
||||
}
|
||||
|
||||
public Task<AuditBundleListResponse> ListBundlesAsync(
|
||||
string? subjectName,
|
||||
string? status,
|
||||
int limit,
|
||||
string? continuationToken,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<AuditBundleJob> query = _jobs.Values;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subjectName))
|
||||
{
|
||||
query = query.Where(j => j.Subject.Name.Contains(subjectName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
query = query.Where(j => string.Equals(j.Status, status, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// Deterministic ordering: createdAt desc
|
||||
var sorted = query.OrderByDescending(j => j.CreatedAt).ToList();
|
||||
|
||||
var offset = 0;
|
||||
if (!string.IsNullOrEmpty(continuationToken) && int.TryParse(continuationToken, out var parsedOffset))
|
||||
{
|
||||
offset = parsedOffset;
|
||||
}
|
||||
|
||||
var page = sorted.Skip(offset).Take(limit).ToList();
|
||||
var hasMore = offset + page.Count < sorted.Count;
|
||||
var nextToken = hasMore ? (offset + page.Count).ToString() : null;
|
||||
|
||||
var bundles = page.Select(j => new AuditBundleSummary(
|
||||
j.BundleId,
|
||||
j.Subject,
|
||||
j.Status,
|
||||
j.CreatedAt,
|
||||
j.CompletedAt,
|
||||
j.BundleHash,
|
||||
j.Index?.Artifacts.Count ?? 0,
|
||||
j.Index?.VexDecisions?.Count ?? 0)).ToList();
|
||||
|
||||
return Task.FromResult(new AuditBundleListResponse(bundles, nextToken, hasMore));
|
||||
}
|
||||
|
||||
public Task<AuditBundleStatus?> GetBundleStatusAsync(
|
||||
string bundleId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_jobs.TryGetValue(bundleId, out var job))
|
||||
{
|
||||
return Task.FromResult<AuditBundleStatus?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<AuditBundleStatus?>(new AuditBundleStatus(
|
||||
job.BundleId,
|
||||
job.Status,
|
||||
job.Progress,
|
||||
job.CreatedAt,
|
||||
job.CompletedAt,
|
||||
job.BundleHash,
|
||||
job.Status == "Completed" ? $"/v1/audit-bundles/{job.BundleId}/download" : null,
|
||||
job.OciReference,
|
||||
job.ErrorCode,
|
||||
job.ErrorMessage));
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetBundleContentAsync(
|
||||
string bundleId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_jobs.TryGetValue(bundleId, out var job) || job.Content is null)
|
||||
{
|
||||
return Task.FromResult<byte[]?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<byte[]?>(job.Content);
|
||||
}
|
||||
|
||||
public Task<AuditBundleIndexDto?> GetBundleIndexAsync(
|
||||
string bundleId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_jobs.TryGetValue(bundleId, out var job) || job.Index is null)
|
||||
{
|
||||
return Task.FromResult<AuditBundleIndexDto?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<AuditBundleIndexDto?>(job.Index);
|
||||
}
|
||||
|
||||
private async Task ProcessBundleAsync(string bundleId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_jobs.TryGetValue(bundleId, out var job))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
job.Status = "Running";
|
||||
job.Progress = 10;
|
||||
|
||||
// Simulate gathering artifacts
|
||||
await Task.Delay(500, cancellationToken);
|
||||
job.Progress = 30;
|
||||
|
||||
// Create bundle content
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
|
||||
{
|
||||
var artifacts = new List<BundleArtifactDto>();
|
||||
|
||||
// Add sample vulnerability report if requested
|
||||
if (job.IncludeContent.VulnReports)
|
||||
{
|
||||
var vulnReport = CreateSampleVulnReport(job.Subject);
|
||||
var vulnEntry = archive.CreateEntry("reports/vuln-report.json");
|
||||
await using var vulnWriter = new StreamWriter(vulnEntry.Open());
|
||||
await vulnWriter.WriteAsync(vulnReport);
|
||||
|
||||
var vulnDigest = ComputeHash(vulnReport);
|
||||
artifacts.Add(new BundleArtifactDto(
|
||||
"vuln-report-1",
|
||||
"VULN_REPORT",
|
||||
"StellaOps@1.0.0",
|
||||
"reports/vuln-report.json",
|
||||
"application/json",
|
||||
new Dictionary<string, string> { ["sha256"] = vulnDigest },
|
||||
null));
|
||||
}
|
||||
|
||||
job.Progress = 50;
|
||||
|
||||
// Add sample SBOM if requested
|
||||
if (job.IncludeContent.Sbom)
|
||||
{
|
||||
var sbom = CreateSampleSbom(job.Subject);
|
||||
var sbomEntry = archive.CreateEntry("sbom/cyclonedx.json");
|
||||
await using var sbomWriter = new StreamWriter(sbomEntry.Open());
|
||||
await sbomWriter.WriteAsync(sbom);
|
||||
|
||||
var sbomDigest = ComputeHash(sbom);
|
||||
artifacts.Add(new BundleArtifactDto(
|
||||
"sbom-cyclonedx",
|
||||
"SBOM",
|
||||
"Syft@1.0.0",
|
||||
"sbom/cyclonedx.json",
|
||||
"application/vnd.cyclonedx+json",
|
||||
new Dictionary<string, string> { ["sha256"] = sbomDigest },
|
||||
null));
|
||||
}
|
||||
|
||||
job.Progress = 70;
|
||||
|
||||
// Create the index
|
||||
var vexDecisions = new List<BundleVexDecisionEntryDto>();
|
||||
var integrity = new BundleIntegrityDto(
|
||||
ComputeHash(string.Join(",", artifacts.Select(a => a.Digest["sha256"]))),
|
||||
"sha256");
|
||||
|
||||
var index = new AuditBundleIndexDto(
|
||||
"stella.ops/v1",
|
||||
"AuditBundleIndex",
|
||||
job.BundleId,
|
||||
DateTimeOffset.UtcNow,
|
||||
job.CreatedBy,
|
||||
job.Subject,
|
||||
job.TimeWindow,
|
||||
artifacts,
|
||||
vexDecisions,
|
||||
integrity);
|
||||
|
||||
job.Index = index;
|
||||
|
||||
// Write index to archive
|
||||
var indexJson = JsonSerializer.Serialize(index, JsonOptions);
|
||||
var indexEntry = archive.CreateEntry("audit-bundle-index.json");
|
||||
await using var indexWriter = new StreamWriter(indexEntry.Open());
|
||||
await indexWriter.WriteAsync(indexJson);
|
||||
}
|
||||
|
||||
job.Progress = 90;
|
||||
|
||||
// Get the content
|
||||
job.Content = memoryStream.ToArray();
|
||||
job.BundleHash = ComputeHash(job.Content);
|
||||
|
||||
job.Status = "Completed";
|
||||
job.Progress = 100;
|
||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Completed audit bundle {BundleId} with hash {BundleHash}",
|
||||
bundleId, job.BundleHash);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process audit bundle {BundleId}", bundleId);
|
||||
job.Status = "Failed";
|
||||
job.ErrorCode = "PROCESSING_ERROR";
|
||||
job.ErrorMessage = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateSampleVulnReport(BundleSubjectRefDto subject)
|
||||
{
|
||||
var report = new
|
||||
{
|
||||
subject = subject.Name,
|
||||
scanDate = DateTimeOffset.UtcNow.ToString("O"),
|
||||
findings = new[]
|
||||
{
|
||||
new { id = "CVE-2023-12345", severity = "HIGH", package = "sample-pkg", version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
return JsonSerializer.Serialize(report, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateSampleSbom(BundleSubjectRefDto subject)
|
||||
{
|
||||
var sbom = new
|
||||
{
|
||||
bomFormat = "CycloneDX",
|
||||
specVersion = "1.6",
|
||||
metadata = new
|
||||
{
|
||||
component = new { name = subject.Name, type = "container" }
|
||||
},
|
||||
components = Array.Empty<object>()
|
||||
};
|
||||
return JsonSerializer.Serialize(sbom, JsonOptions);
|
||||
}
|
||||
|
||||
private static string ComputeHash(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
return ComputeHash(bytes);
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] bytes)
|
||||
{
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
private sealed class AuditBundleJob
|
||||
{
|
||||
public required string BundleId { get; init; }
|
||||
public required string Status { get; set; }
|
||||
public required int Progress { get; set; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? CompletedAt { get; set; }
|
||||
public required BundleActorRefDto CreatedBy { get; init; }
|
||||
public required BundleSubjectRefDto Subject { get; init; }
|
||||
public BundleTimeWindowDto? TimeWindow { get; init; }
|
||||
public required AuditBundleContentSelection IncludeContent { get; init; }
|
||||
public string? BundleHash { get; set; }
|
||||
public string? OciReference { get; set; }
|
||||
public string? ErrorCode { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public byte[]? Content { get; set; }
|
||||
public AuditBundleIndexDto? Index { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace StellaOps.ExportCenter.WebService.AuditBundle;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering audit bundle services.
|
||||
/// </summary>
|
||||
public static class AuditBundleServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds audit bundle job handler services.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddAuditBundleJobHandler(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IAuditBundleJobHandler, AuditBundleJobHandler>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using StellaOps.ExportCenter.Client.Models;
|
||||
|
||||
namespace StellaOps.ExportCenter.WebService.AuditBundle;
|
||||
|
||||
/// <summary>
|
||||
/// Handles audit bundle job operations.
|
||||
/// </summary>
|
||||
public interface IAuditBundleJobHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new audit bundle.
|
||||
/// </summary>
|
||||
Task<AuditBundleCreateResult> CreateBundleAsync(
|
||||
CreateAuditBundleRequest request,
|
||||
string actorId,
|
||||
string actorName,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lists audit bundles with optional filtering.
|
||||
/// </summary>
|
||||
Task<AuditBundleListResponse> ListBundlesAsync(
|
||||
string? subjectName,
|
||||
string? status,
|
||||
int limit,
|
||||
string? continuationToken,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of a specific audit bundle.
|
||||
/// </summary>
|
||||
Task<AuditBundleStatus?> GetBundleStatusAsync(
|
||||
string bundleId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content of a completed audit bundle.
|
||||
/// </summary>
|
||||
Task<byte[]?> GetBundleContentAsync(
|
||||
string bundleId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index manifest of a completed audit bundle.
|
||||
/// </summary>
|
||||
Task<AuditBundleIndexDto?> GetBundleIndexAsync(
|
||||
string bundleId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from audit bundle creation.
|
||||
/// </summary>
|
||||
public sealed record AuditBundleCreateResult(
|
||||
CreateAuditBundleResponse? Response,
|
||||
ErrorDetail? Error);
|
||||
@@ -12,6 +12,7 @@ using StellaOps.ExportCenter.WebService.Attestation;
|
||||
using StellaOps.ExportCenter.WebService.Incident;
|
||||
using StellaOps.ExportCenter.WebService.RiskBundle;
|
||||
using StellaOps.ExportCenter.WebService.SimulationExport;
|
||||
using StellaOps.ExportCenter.WebService.AuditBundle;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -72,6 +73,9 @@ builder.Services.AddRiskBundleJobHandler();
|
||||
// Simulation export services
|
||||
builder.Services.AddSimulationExport();
|
||||
|
||||
// Audit bundle job handler
|
||||
builder.Services.AddAuditBundleJobHandler();
|
||||
|
||||
// Export API services (profiles, runs, artifacts)
|
||||
builder.Services.AddExportApiServices(options =>
|
||||
{
|
||||
@@ -112,6 +116,9 @@ app.MapRiskBundleEndpoints();
|
||||
// Simulation export endpoints
|
||||
app.MapSimulationExportEndpoints();
|
||||
|
||||
// Audit bundle endpoints
|
||||
app.MapAuditBundleEndpoints();
|
||||
|
||||
// Export API endpoints (profiles, runs, artifacts, SSE)
|
||||
app.MapExportApiEndpoints();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Infrastructure\StellaOps.ExportCenter.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Client\StellaOps.ExportCenter.Client.csproj" />
|
||||
<ProjectReference Include="..\..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="..\..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user