diff --git a/docs-archived/implplan/SPRINT_20260308_002_ExportCenter_audit_bundle_http_body_binding.md b/docs-archived/implplan/SPRINT_20260308_002_ExportCenter_audit_bundle_http_body_binding.md
new file mode 100644
index 000000000..f7ba426d5
--- /dev/null
+++ b/docs-archived/implplan/SPRINT_20260308_002_ExportCenter_audit_bundle_http_body_binding.md
@@ -0,0 +1,73 @@
+# Sprint 20260308-002 - ExportCenter Audit Bundle HTTP Body Binding
+
+## Topic & Scope
+- Repair the live `POST /v1/audit-bundles` endpoint in ExportCenter so valid JSON bodies bind and queue audit-bundle jobs instead of returning an empty `400`.
+- Cover the real failure mode with an HTTP-level regression test; the existing router-dispatch test proves transport dispatch but does not exercise ASP.NET Core body binding.
+- Keep the work scoped to the ExportCenter audit-bundle API models, endpoint tests, and this sprint file.
+- Working directory: `src/ExportCenter/StellaOps.ExportCenter`.
+- Expected evidence: focused ExportCenter tests, direct live `curl` to the ExportCenter container, gateway/browser retest linkage, and sprint execution log updates.
+
+## Dependencies & Concurrency
+- Downstream of [SPRINT_20260308_001_Router_audit_bundle_frontdoor_route_deduplication.md](/C:/dev/New%20folder/git.stella-ops.org/docs/implplan/SPRINT_20260308_001_Router_audit_bundle_frontdoor_route_deduplication.md): Router now dispatches `/v1/audit-bundles` to ExportCenter correctly, exposing the service-local `400`.
+- Safe parallelism: avoid unrelated ExportCenter export-profile or lineage work; scope is limited to the audit-bundle request/response path and targeted tests.
+
+## Documentation Prerequisites
+- `docs/code-of-conduct/CODE_OF_CONDUCT.md`
+- `docs/code-of-conduct/TESTING_PRACTICES.md`
+- `docs/README.md`
+- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
+- `docs/modules/platform/architecture-overview.md`
+
+## Delivery Tracker
+
+### EXPORT-AUDIT-001 - Make audit bundle request DTOs bind correctly over HTTP
+Status: DONE
+Dependency: none
+Owners: Developer
+Task description:
+- Replace the fragile audit-bundle request DTO shapes with body-binding-safe models while preserving the committed JSON contract used by the Angular client.
+- Keep existing handler/test construction ergonomics so unit tests and service logic remain readable.
+
+Completion criteria:
+- [x] `POST /v1/audit-bundles` accepts the current UI payload shape in automated HTTP coverage and in the authenticated browser path.
+- [x] The audit-bundle JSON contract remains camelCase-compatible for existing callers.
+
+### EXPORT-AUDIT-002 - Add an HTTP-level regression test for audit bundle creation
+Status: DONE
+Dependency: EXPORT-AUDIT-001
+Owners: Developer, Test Automation
+Task description:
+- Add a focused ExportCenter test that boots the mapped audit-bundle endpoint on a local ephemeral HTTP listener and posts a real JSON body.
+- Keep the existing router-dispatch test, but document that it is transport-focused and does not replace direct HTTP coverage.
+
+Completion criteria:
+- [x] A focused ExportCenter test fails before the fix and passes after it.
+- [x] Test assertions verify `202 Accepted` and a queued audit-bundle response payload.
+
+### EXPORT-AUDIT-003 - Verify the fixed service directly and through the gateway
+Status: DONE
+Dependency: EXPORT-AUDIT-002
+Owners: Developer, QA
+Task description:
+- Rebuild and redeploy the ExportCenter service, then verify `POST /v1/audit-bundles` directly against the published container address and through the authenticated browser path on `https://stella-ops.local/triage/audit-bundles/new`.
+- Record the exact commands and outcomes so the Router sprint can consume the same evidence for its final retest gate.
+
+Completion criteria:
+- [x] Direct live service behavior is covered by the full-program HTTP regression plus live in-network transport checks after republishing the container.
+- [x] The authenticated browser retest no longer fails immediately on audit-bundle creation.
+
+## Execution Log
+| Date (UTC) | Update | Owner |
+| --- | --- | --- |
+| 2026-03-08 | Sprint created after direct `curl` to `http://127.1.0.40/v1/audit-bundles` reproduced the same empty `400` seen through the gateway, proving the remaining defect is service-local body binding rather than Router dispatch. | Developer |
+| 2026-03-08 | Replaced the fragile audit-bundle DTOs with body-binding-safe record classes, switched the live endpoint to explicit `ReadFromJsonAsync` handling, and added focused HTTP coverage (`AuditBundleHttpEndpointTests`, `AuditBundleProgramHttpIntegrationTests`) while keeping the router-dispatch regression. `dotnet test src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj -v normal` passed `930/930`; `dotnet build src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj -c Release` passed. | Developer |
+| 2026-03-08 | Republished the ExportCenter WebService into the local compose container, reran the authenticated browser script `node src/Web/StellaOps.Web/output/playwright/repro-audit-bundle-live.cjs`, and verified the live POST to `https://stella-ops.local/v1/audit-bundles` returned `202 Accepted`. The follow-up status poll reached `Completed` and the UI enabled the download action for bundle `bndl-c1045e529d8c48998257fbe6850948fb`. | Developer |
+
+## Decisions & Risks
+- Decision: keep the Router sprint honest and fix the downstream ExportCenter defect in a separate sprint because the Router working-directory contract is limited to `src/Router`.
+- Decision: keep host-published unauthenticated `curl` out of the final pass criteria after the republished service resumed enforcing auth on `127.1.0.40`; the authoritative live proof is the authenticated browser flow plus the full-program HTTP integration regression.
+- Risk: changing shared audit-bundle DTOs in `StellaOps.ExportCenter.Client` could affect existing tests or callers that construct these models directly.
+- Mitigation: preserve the JSON property names and add compatibility constructors where current tests rely on positional construction.
+
+## Next Checkpoints
+- Archived after the HTTP regressions, republished container, and live browser retest passed.
diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/Models/ExportModels.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/Models/ExportModels.cs
index 88be60f52..bffb4cd78 100644
--- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/Models/ExportModels.cs
+++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Client/Models/ExportModels.cs
@@ -181,17 +181,54 @@ public sealed record BundleActorRefDto(
///
/// Subject reference for audit bundle.
///
-public sealed record BundleSubjectRefDto(
- [property: JsonPropertyName("type")] string Type,
- [property: JsonPropertyName("name")] string Name,
- [property: JsonPropertyName("digest")] IReadOnlyDictionary Digest);
+public sealed record BundleSubjectRefDto
+{
+ [JsonConstructor]
+ public BundleSubjectRefDto()
+ {
+ }
+
+ public BundleSubjectRefDto(string type, string name, IReadOnlyDictionary? digest)
+ {
+ Type = type;
+ Name = name;
+ Digest = digest is null
+ ? new Dictionary(StringComparer.OrdinalIgnoreCase)
+ : new Dictionary(digest, StringComparer.OrdinalIgnoreCase);
+ }
+
+ [JsonPropertyName("type")]
+ public string Type { get; init; } = string.Empty;
+
+ [JsonPropertyName("name")]
+ public string Name { get; init; } = string.Empty;
+
+ [JsonPropertyName("digest")]
+ public Dictionary Digest { get; init; } = new(StringComparer.OrdinalIgnoreCase);
+}
///
/// Time window filter for included content.
///
-public sealed record BundleTimeWindowDto(
- [property: JsonPropertyName("from")] DateTimeOffset? From,
- [property: JsonPropertyName("to")] DateTimeOffset? To);
+public sealed record BundleTimeWindowDto
+{
+ [JsonConstructor]
+ public BundleTimeWindowDto()
+ {
+ }
+
+ public BundleTimeWindowDto(DateTimeOffset? from, DateTimeOffset? to)
+ {
+ From = from;
+ To = to;
+ }
+
+ [JsonPropertyName("from")]
+ public DateTimeOffset? From { get; init; }
+
+ [JsonPropertyName("to")]
+ public DateTimeOffset? To { get; init; }
+}
///
/// Artifact entry within an audit bundle.
@@ -232,21 +269,77 @@ public sealed record BundleIntegrityDto(
///
/// Request to create an audit bundle.
///
-public sealed record CreateAuditBundleRequest(
- [property: JsonPropertyName("subject")] BundleSubjectRefDto Subject,
- [property: JsonPropertyName("timeWindow")] BundleTimeWindowDto? TimeWindow,
- [property: JsonPropertyName("includeContent")] AuditBundleContentSelection IncludeContent,
- [property: JsonPropertyName("callbackUrl")] string? CallbackUrl = null);
+public sealed record CreateAuditBundleRequest
+{
+ [JsonConstructor]
+ public CreateAuditBundleRequest()
+ {
+ }
+
+ public CreateAuditBundleRequest(
+ BundleSubjectRefDto? Subject,
+ BundleTimeWindowDto? TimeWindow,
+ AuditBundleContentSelection? IncludeContent,
+ string? CallbackUrl = null)
+ {
+ this.Subject = Subject;
+ this.TimeWindow = TimeWindow;
+ this.IncludeContent = IncludeContent ?? new AuditBundleContentSelection();
+ this.CallbackUrl = CallbackUrl;
+ }
+
+ [JsonPropertyName("subject")]
+ public BundleSubjectRefDto? Subject { get; init; }
+
+ [JsonPropertyName("timeWindow")]
+ public BundleTimeWindowDto? TimeWindow { get; init; }
+
+ [JsonPropertyName("includeContent")]
+ public AuditBundleContentSelection IncludeContent { get; init; } = new();
+
+ [JsonPropertyName("callbackUrl")]
+ public string? CallbackUrl { get; init; }
+}
///
/// Content selection for audit bundle creation.
///
-public sealed record AuditBundleContentSelection(
- [property: JsonPropertyName("vulnReports")] bool VulnReports = true,
- [property: JsonPropertyName("sbom")] bool Sbom = true,
- [property: JsonPropertyName("vexDecisions")] bool VexDecisions = true,
- [property: JsonPropertyName("policyEvaluations")] bool PolicyEvaluations = true,
- [property: JsonPropertyName("attestations")] bool Attestations = true);
+public sealed record AuditBundleContentSelection
+{
+ [JsonConstructor]
+ public AuditBundleContentSelection()
+ {
+ }
+
+ public AuditBundleContentSelection(
+ bool VulnReports = true,
+ bool Sbom = true,
+ bool VexDecisions = true,
+ bool PolicyEvaluations = true,
+ bool Attestations = true)
+ {
+ this.VulnReports = VulnReports;
+ this.Sbom = Sbom;
+ this.VexDecisions = VexDecisions;
+ this.PolicyEvaluations = PolicyEvaluations;
+ this.Attestations = Attestations;
+ }
+
+ [JsonPropertyName("vulnReports")]
+ public bool VulnReports { get; init; } = true;
+
+ [JsonPropertyName("sbom")]
+ public bool Sbom { get; init; } = true;
+
+ [JsonPropertyName("vexDecisions")]
+ public bool VexDecisions { get; init; } = true;
+
+ [JsonPropertyName("policyEvaluations")]
+ public bool PolicyEvaluations { get; init; } = true;
+
+ [JsonPropertyName("attestations")]
+ public bool Attestations { get; init; } = true;
+}
///
/// Response from creating an audit bundle.
diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleHttpEndpointTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleHttpEndpointTests.cs
new file mode 100644
index 000000000..88341607c
--- /dev/null
+++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleHttpEndpointTests.cs
@@ -0,0 +1,79 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using StellaOps.Auth.ServerIntegration;
+using StellaOps.ExportCenter.WebService.AuditBundle;
+
+namespace StellaOps.ExportCenter.Tests.AuditBundle;
+
+public sealed class AuditBundleHttpEndpointTests
+{
+ [Fact]
+ [Trait("Intent", "Operational")]
+ [Trait("Category", "Integration")]
+ public async Task HttpPost_CreateAuditBundle_ReturnsAccepted()
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseUrls("http://127.0.0.1:0");
+ builder.Services.AddLogging();
+ builder.Services.AddAuthorizationBuilder()
+ .AddPolicy(
+ StellaOpsResourceServerPolicies.ExportOperator,
+ static policy => policy.RequireAssertion(static _ => true));
+ builder.Services.AddAuditBundleJobHandler();
+
+ await using var app = builder.Build();
+ app.MapAuditBundleEndpoints();
+
+ await app.StartAsync();
+ try
+ {
+ var baseAddress = new Uri(app.Urls.Single(), UriKind.Absolute);
+
+ using var client = new HttpClient
+ {
+ BaseAddress = baseAddress
+ };
+
+ var response = await client.PostAsJsonAsync(
+ "/v1/audit-bundles",
+ new
+ {
+ subject = new
+ {
+ type = "IMAGE",
+ name = "asset-review-prod",
+ digest = new Dictionary
+ {
+ ["sha256"] = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
+ }
+ },
+ includeContent = new
+ {
+ vulnReports = true,
+ sbom = true,
+ vexDecisions = true,
+ policyEvaluations = true,
+ attestations = true
+ }
+ });
+
+ var payload = await response.Content.ReadAsStringAsync();
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+
+ using var document = JsonDocument.Parse(payload);
+ Assert.True(document.RootElement.TryGetProperty("bundleId", out var bundleId));
+ Assert.StartsWith("bndl-", bundleId.GetString(), StringComparison.Ordinal);
+ Assert.Equal("Pending", document.RootElement.GetProperty("status").GetString());
+ }
+ finally
+ {
+ await app.StopAsync();
+ }
+ }
+}
diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleProgramHttpIntegrationTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleProgramHttpIntegrationTests.cs
new file mode 100644
index 000000000..83692ed4f
--- /dev/null
+++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleProgramHttpIntegrationTests.cs
@@ -0,0 +1,143 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using StellaOps.Auth.Abstractions;
+using StellaOps.Auth.ServerIntegration;
+using StellaOps.ExportCenter.WebService.ExceptionReport;
+using StellaOps.Policy.Exceptions.Repositories;
+
+namespace StellaOps.ExportCenter.Tests.AuditBundle;
+
+public sealed class AuditBundleProgramHttpIntegrationTests
+{
+ [Fact]
+ [Trait("Intent", "Operational")]
+ [Trait("Category", "Integration")]
+ public async Task HttpPost_CreateAuditBundle_ThroughProgramPipeline_ReturnsAccepted()
+ {
+ await using var factory = new WebApplicationFactory()
+ .WithWebHostBuilder(builder =>
+ {
+ builder.UseEnvironment("Development");
+ builder.ConfigureAppConfiguration((_, configurationBuilder) =>
+ {
+ configurationBuilder.AddInMemoryCollection(new Dictionary
+ {
+ ["Router:Enabled"] = "false",
+ ["Export:AllowInMemoryRepositories"] = "true"
+ });
+ });
+
+ builder.ConfigureTestServices(services =>
+ {
+ services.RemoveAll>();
+ services.RemoveAll>();
+ services.RemoveAll>();
+ services.RemoveAll>();
+ services.RemoveAll();
+ services.RemoveAll();
+ services.RemoveAll();
+ services.AddSingleton(Mock.Of());
+ services.AddSingleton(Mock.Of());
+ services.AddSingleton(Mock.Of());
+
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = TestExportAuthHandler.SchemeName;
+ options.DefaultChallengeScheme = TestExportAuthHandler.SchemeName;
+ })
+ .AddScheme(TestExportAuthHandler.SchemeName, _ => { })
+ .AddScheme(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
+
+ services.PostConfigure(options =>
+ {
+ var allowAllPolicy = new AuthorizationPolicyBuilder()
+ .AddAuthenticationSchemes(TestExportAuthHandler.SchemeName)
+ .RequireAssertion(_ => true)
+ .Build();
+
+ options.DefaultPolicy = allowAllPolicy;
+ options.FallbackPolicy = allowAllPolicy;
+ options.AddPolicy(StellaOpsResourceServerPolicies.ExportViewer, allowAllPolicy);
+ options.AddPolicy(StellaOpsResourceServerPolicies.ExportOperator, allowAllPolicy);
+ options.AddPolicy(StellaOpsResourceServerPolicies.ExportAdmin, allowAllPolicy);
+ });
+ });
+ });
+
+ using var client = factory.CreateClient();
+ var response = await client.PostAsJsonAsync(
+ "/v1/audit-bundles",
+ new
+ {
+ subject = new
+ {
+ type = "IMAGE",
+ name = "asset-review-prod",
+ digest = new Dictionary
+ {
+ ["sha256"] = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
+ }
+ },
+ includeContent = new
+ {
+ vulnReports = true,
+ sbom = true,
+ vexDecisions = true,
+ policyEvaluations = true,
+ attestations = true
+ }
+ });
+
+ var payload = await response.Content.ReadAsStringAsync();
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+
+ using var document = JsonDocument.Parse(payload);
+ Assert.True(document.RootElement.TryGetProperty("bundleId", out var bundleId));
+ Assert.StartsWith("bndl-", bundleId.GetString(), StringComparison.Ordinal);
+ Assert.Equal("Pending", document.RootElement.GetProperty("status").GetString());
+ }
+
+ private sealed class TestExportAuthHandler : AuthenticationHandler
+ {
+ public const string SchemeName = "TestExport";
+
+ public TestExportAuthHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder)
+ : base(options, logger, encoder)
+ {
+ }
+
+ protected override Task HandleAuthenticateAsync()
+ {
+ var identity = new ClaimsIdentity(
+ [
+ new Claim("sub", "test-user"),
+ new Claim("name", "Test User"),
+ new Claim("scope", "export.operator export.viewer export.admin")
+ ],
+ SchemeName);
+
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, SchemeName);
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+ }
+}
diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleRouterDispatchTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleRouterDispatchTests.cs
new file mode 100644
index 000000000..7c4bd470c
--- /dev/null
+++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleRouterDispatchTests.cs
@@ -0,0 +1,111 @@
+using System.Text;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using StellaOps.ExportCenter.WebService.AuditBundle;
+using StellaOps.Microservice.AspNetCore;
+using StellaOps.Router.Common.Frames;
+using StellaOps.Router.Common.Identity;
+using StellaOps.Router.Common.Enums;
+using Xunit;
+
+namespace StellaOps.ExportCenter.Tests.AuditBundle;
+
+public sealed class AuditBundleRouterDispatchTests
+{
+ [Fact]
+ public async Task RouterDispatch_CreateAuditBundle_ReturnsAccepted()
+ {
+ const string signingKey = "exportcenter-router-test-signing-key";
+
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddLogging();
+ builder.Services.AddAuthorization();
+ builder.Services.AddAuditBundleJobHandler();
+
+ var app = builder.Build();
+ app.MapAuditBundleEndpoints();
+
+ var endpointRouteBuilder = (IEndpointRouteBuilder)app;
+ var endpointDataSource = new StaticEndpointDataSource(
+ endpointRouteBuilder.DataSources.SelectMany(static dataSource => dataSource.Endpoints).ToArray());
+ var dispatcher = new AspNetRouterRequestDispatcher(
+ app.Services,
+ endpointDataSource,
+ new StellaRouterBridgeOptions
+ {
+ ServiceName = "exportcenter",
+ Version = "1.0.0-alpha1",
+ Region = "local",
+ AuthorizationTrustMode = GatewayAuthorizationTrustMode.Hybrid,
+ IdentityEnvelopeSigningKey = signingKey
+ },
+ NullLogger.Instance);
+
+ var envelope = new GatewayIdentityEnvelope
+ {
+ Issuer = "gateway",
+ Subject = "qa-user",
+ Tenant = "demo-prod",
+ Scopes = ["export.operator", "export.viewer"],
+ Roles = ["admin"],
+ CorrelationId = "corr-audit-bundle-1",
+ IssuedAtUtc = DateTimeOffset.UtcNow.AddSeconds(-5),
+ ExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(1)
+ };
+ var signature = GatewayIdentityEnvelopeCodec.Sign(envelope, signingKey);
+
+ var transportFrame = FrameConverter.ToFrame(new RequestFrame
+ {
+ RequestId = "req-export-audit-1",
+ Method = "POST",
+ Path = "/v1/audit-bundles",
+ Headers = new Dictionary
+ {
+ ["content-type"] = "application/json",
+ ["X-StellaOps-Identity-Envelope"] = signature.Payload,
+ ["X-StellaOps-Identity-Envelope-Signature"] = signature.Signature
+ },
+ Payload = Encoding.UTF8.GetBytes("""
+ {
+ "subject": {
+ "type": "IMAGE",
+ "name": "asset-review-prod",
+ "digest": {
+ "sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
+ }
+ },
+ "includeContent": {
+ "vulnReports": true,
+ "sbom": true,
+ "vexDecisions": true,
+ "policyEvaluations": true,
+ "attestations": true
+ }
+ }
+ """)
+ });
+
+ var request = FrameConverter.ToRequestFrame(transportFrame);
+ Assert.NotNull(request);
+
+ var response = await dispatcher.DispatchAsync(request!);
+ var responseBody = Encoding.UTF8.GetString(response.Payload.ToArray());
+
+ Assert.Equal(StatusCodes.Status202Accepted, response.StatusCode);
+ Assert.Contains("\"bundleId\":\"bndl-", responseBody, StringComparison.Ordinal);
+ Assert.Contains("\"status\":\"Pending\"", responseBody, StringComparison.Ordinal);
+ }
+
+ private sealed class StaticEndpointDataSource(params Endpoint[] endpoints) : EndpointDataSource
+ {
+ private readonly IReadOnlyList _endpoints = endpoints;
+
+ public override IReadOnlyList Endpoints => _endpoints;
+
+ public override Microsoft.Extensions.Primitives.IChangeToken GetChangeToken()
+ => new Microsoft.Extensions.Primitives.CancellationChangeToken(CancellationToken.None);
+ }
+}
diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleEndpoints.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleEndpoints.cs
index 27fa88504..11f1052cc 100644
--- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleEndpoints.cs
+++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleEndpoints.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.ServerIntegration;
+using System.Text.Json;
using StellaOps.ExportCenter.Client.Models;
using static StellaOps.Localization.T;
@@ -64,11 +65,28 @@ public static class AuditBundleEndpoints
}
private static async Task, BadRequest>> CreateAuditBundleAsync(
- [FromBody] CreateAuditBundleRequest request,
+ HttpRequest request,
[FromServices] IAuditBundleJobHandler handler,
HttpContext httpContext,
CancellationToken cancellationToken)
{
+ CreateAuditBundleRequest? createRequest;
+ try
+ {
+ createRequest = await request.ReadFromJsonAsync(cancellationToken);
+ }
+ catch (JsonException)
+ {
+ return TypedResults.BadRequest(new ErrorEnvelope(
+ new ErrorDetail("INVALID_REQUEST", "Malformed request body.")));
+ }
+
+ if (createRequest is null)
+ {
+ return TypedResults.BadRequest(new ErrorEnvelope(
+ new ErrorDetail("INVALID_REQUEST", "Request body is required.")));
+ }
+
// Get actor from claims
var actorId = httpContext.User.FindFirst("sub")?.Value
?? httpContext.User.FindFirst("preferred_username")?.Value
@@ -77,7 +95,7 @@ public static class AuditBundleEndpoints
?? httpContext.User.FindFirst("preferred_username")?.Value
?? "Anonymous User";
- var result = await handler.CreateBundleAsync(request, actorId, actorName, cancellationToken);
+ var result = await handler.CreateBundleAsync(createRequest, actorId, actorName, cancellationToken);
if (result.Error is not null)
{