up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 23:44:42 +02:00
parent ef6e4b2067
commit 3b96b2e3ea
298 changed files with 47516 additions and 1168 deletions

View File

@@ -0,0 +1,238 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.RiskProfile.Export;
namespace StellaOps.Policy.Engine.Endpoints;
internal static class ProfileExportEndpoints
{
public static IEndpointRouteBuilder MapProfileExport(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/risk/profiles/export")
.RequireAuthorization()
.WithTags("Profile Export/Import");
group.MapPost("/", ExportProfiles)
.WithName("ExportProfiles")
.WithSummary("Export risk profiles as a signed bundle.")
.Produces<ExportResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
group.MapPost("/download", DownloadBundle)
.WithName("DownloadProfileBundle")
.WithSummary("Export and download risk profiles as a JSON file.")
.Produces<FileContentHttpResult>(StatusCodes.Status200OK, contentType: "application/json");
endpoints.MapPost("/api/risk/profiles/import", ImportProfiles)
.RequireAuthorization()
.WithName("ImportProfiles")
.WithSummary("Import risk profiles from a signed bundle.")
.WithTags("Profile Export/Import")
.Produces<ImportResponse>(StatusCodes.Status200OK)
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
endpoints.MapPost("/api/risk/profiles/verify", VerifyBundle)
.RequireAuthorization()
.WithName("VerifyProfileBundle")
.WithSummary("Verify the signature of a profile bundle without importing.")
.WithTags("Profile Export/Import")
.Produces<VerifyResponse>(StatusCodes.Status200OK);
return endpoints;
}
private static IResult ExportProfiles(
HttpContext context,
[FromBody] ExportProfilesRequest request,
RiskProfileConfigurationService profileService,
ProfileExportService exportService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
if (request == null || request.ProfileIds == null || request.ProfileIds.Count == 0)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "At least one profile ID is required.",
Status = StatusCodes.Status400BadRequest
});
}
var profiles = new List<StellaOps.Policy.RiskProfile.Models.RiskProfileModel>();
var notFound = new List<string>();
foreach (var profileId in request.ProfileIds)
{
var profile = profileService.GetProfile(profileId);
if (profile != null)
{
profiles.Add(profile);
}
else
{
notFound.Add(profileId);
}
}
if (notFound.Count > 0)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Profiles not found",
Detail = $"The following profiles were not found: {string.Join(", ", notFound)}",
Status = StatusCodes.Status400BadRequest
});
}
var actorId = ResolveActorId(context);
var bundle = exportService.Export(profiles, request, actorId);
return Results.Ok(new ExportResponse(bundle));
}
private static IResult DownloadBundle(
HttpContext context,
[FromBody] ExportProfilesRequest request,
RiskProfileConfigurationService profileService,
ProfileExportService exportService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
if (request == null || request.ProfileIds == null || request.ProfileIds.Count == 0)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "At least one profile ID is required.",
Status = StatusCodes.Status400BadRequest
});
}
var profiles = new List<StellaOps.Policy.RiskProfile.Models.RiskProfileModel>();
foreach (var profileId in request.ProfileIds)
{
var profile = profileService.GetProfile(profileId);
if (profile != null)
{
profiles.Add(profile);
}
}
var actorId = ResolveActorId(context);
var bundle = exportService.Export(profiles, request, actorId);
var json = exportService.SerializeBundle(bundle);
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
var fileName = $"risk-profiles-{bundle.BundleId}.json";
return Results.File(bytes, "application/json", fileName);
}
private static IResult ImportProfiles(
HttpContext context,
[FromBody] ImportProfilesRequest request,
RiskProfileConfigurationService profileService,
ProfileExportService exportService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)
{
return scopeResult;
}
if (request == null || request.Bundle == null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Bundle is required.",
Status = StatusCodes.Status400BadRequest
});
}
var actorId = ResolveActorId(context);
// Create an export service with save capability
var importExportService = new ProfileExportService(
timeProvider: TimeProvider.System,
profileLookup: id => profileService.GetProfile(id),
lifecycleLookup: null,
profileSave: profile => profileService.RegisterProfile(profile),
keyLookup: null);
var result = importExportService.Import(request, actorId);
return Results.Ok(new ImportResponse(result));
}
private static IResult VerifyBundle(
HttpContext context,
[FromBody] RiskProfileBundle bundle,
ProfileExportService exportService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
{
return scopeResult;
}
if (bundle == null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "Bundle is required.",
Status = StatusCodes.Status400BadRequest
});
}
var verification = exportService.VerifySignature(bundle);
return Results.Ok(new VerifyResponse(verification, bundle.Metadata));
}
private static string? ResolveActorId(HttpContext context)
{
var user = context.User;
var actor = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? user?.FindFirst(ClaimTypes.Upn)?.Value
?? user?.FindFirst("sub")?.Value;
if (!string.IsNullOrWhiteSpace(actor))
{
return actor;
}
if (context.Request.Headers.TryGetValue("X-StellaOps-Actor", out var header) && !string.IsNullOrWhiteSpace(header))
{
return header.ToString();
}
return null;
}
}
#region Response DTOs
internal sealed record ExportResponse(RiskProfileBundle Bundle);
internal sealed record ImportResponse(ImportResult Result);
internal sealed record VerifyResponse(
SignatureVerificationResult Verification,
BundleMetadata Metadata);
#endregion