633 lines
25 KiB
C#
633 lines
25 KiB
C#
|
|
using Microsoft.AspNetCore.Http.HttpResults;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using StellaOps.Auth.Abstractions;
|
|
using StellaOps.Auth.ServerIntegration;
|
|
using StellaOps.Policy.Engine.Services;
|
|
using StellaOps.Policy.Engine.Tenancy;
|
|
using StellaOps.Policy.RiskProfile.Lifecycle;
|
|
using StellaOps.Policy.RiskProfile.Models;
|
|
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Policy.Engine.Endpoints;
|
|
|
|
/// <summary>
|
|
/// POL-TEN-03: Tenant enforcement via ITenantContextAccessor.
|
|
/// </summary>
|
|
internal static class RiskProfileEndpoints
|
|
{
|
|
public static IEndpointRouteBuilder MapRiskProfiles(this IEndpointRouteBuilder endpoints)
|
|
{
|
|
var group = endpoints.MapGroup("/api/risk/profiles")
|
|
.RequireAuthorization(policy => policy.Requirements.Add(new StellaOpsScopeRequirement(new[] { StellaOpsScopes.PolicyRead })))
|
|
.WithTags("Risk Profiles")
|
|
.RequireTenantContext();
|
|
|
|
group.MapGet(string.Empty, ListProfiles)
|
|
.WithName("ListRiskProfiles")
|
|
.WithSummary("List all available risk profiles.")
|
|
.WithDescription("List all registered risk profiles for the current tenant, returning each profile's identifier, current version, and description. Used by the policy console and advisory pipeline to discover which profiles are available for evaluation binding.")
|
|
.Produces<RiskProfileListResponse>(StatusCodes.Status200OK);
|
|
|
|
group.MapGet("/{profileId}", GetProfile)
|
|
.WithName("GetRiskProfile")
|
|
.WithSummary("Get a risk profile by ID.")
|
|
.WithDescription("Retrieve the full definition of a risk profile by identifier, including signal weights, severity override rules, and the deterministic profile hash used for reproducible evaluation runs.")
|
|
.Produces<RiskProfileResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/{profileId}/versions", ListVersions)
|
|
.WithName("ListRiskProfileVersions")
|
|
.WithSummary("List all versions of a risk profile.")
|
|
.WithDescription("List the full version history of a risk profile, including lifecycle status (Draft, Active, Deprecated, Archived), activation timestamps, and actor identities for each version, supporting compliance audit trails.")
|
|
.Produces<RiskProfileVersionListResponse>(StatusCodes.Status200OK);
|
|
|
|
group.MapGet("/{profileId}/versions/{version}", GetVersion)
|
|
.WithName("GetRiskProfileVersion")
|
|
.WithSummary("Get a specific version of a risk profile.")
|
|
.WithDescription("Retrieve the complete definition and lifecycle metadata for a specific versioned risk profile, including its deterministic hash and version info record, enabling exact-version lookups during policy replay and audit verification.")
|
|
.Produces<RiskProfileResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapPost(string.Empty, CreateProfile)
|
|
.WithName("CreateRiskProfile")
|
|
.WithSummary("Create a new risk profile version in draft status.")
|
|
.WithDescription("Register a new risk profile version in Draft lifecycle status, recording the authoring actor and creation timestamp. The profile must be activated before it can be used in live policy evaluations.")
|
|
.Produces<RiskProfileResponse>(StatusCodes.Status201Created)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
|
|
|
group.MapPost("/{profileId}/versions/{version}:activate", ActivateProfile)
|
|
.WithName("ActivateRiskProfile")
|
|
.WithSummary("Activate a draft risk profile, making it available for use.")
|
|
.WithDescription("Transition a Draft risk profile version to Active status, making it available for binding to policy evaluation runs. Records the activating actor and timestamps the transition for the lifecycle audit log.")
|
|
.Produces<RiskProfileVersionInfoResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapPost("/{profileId}/versions/{version}:deprecate", DeprecateProfile)
|
|
.WithName("DeprecateRiskProfile")
|
|
.WithSummary("Deprecate an active risk profile.")
|
|
.WithDescription("Mark an active risk profile version as Deprecated, optionally specifying a successor version and a human-readable reason. Deprecated profiles remain queryable for audit but are excluded from new evaluation bindings.")
|
|
.Produces<RiskProfileVersionInfoResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapPost("/{profileId}/versions/{version}:archive", ArchiveProfile)
|
|
.WithName("ArchiveRiskProfile")
|
|
.WithSummary("Archive a risk profile, removing it from active use.")
|
|
.WithDescription("Transition a risk profile version to Archived status, permanently removing it from active evaluation use while preserving the definition and lifecycle record for historical audit queries.")
|
|
.Produces<RiskProfileVersionInfoResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/{profileId}/events", GetProfileEvents)
|
|
.WithName("GetRiskProfileEvents")
|
|
.WithSummary("Get lifecycle events for a risk profile.")
|
|
.WithDescription("Retrieve the ordered lifecycle event log for a risk profile, including creation, activation, deprecation, and archival events with actor and timestamp information, supporting compliance reporting and change history review.")
|
|
.Produces<RiskProfileEventListResponse>(StatusCodes.Status200OK);
|
|
|
|
group.MapPost("/compare", CompareProfiles)
|
|
.WithName("CompareRiskProfiles")
|
|
.WithSummary("Compare two risk profile versions and list differences.")
|
|
.WithDescription("Compute a structured diff between two risk profile versions, listing added, removed, and modified signal weights and override rules. Used by policy authors to validate the impact of profile changes before promoting to active.")
|
|
.Produces<RiskProfileComparisonResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
|
|
|
group.MapGet("/{profileId}/hash", GetProfileHash)
|
|
.WithName("GetRiskProfileHash")
|
|
.WithSummary("Get the deterministic hash of a risk profile.")
|
|
.WithDescription("Compute and return the deterministic hash of a risk profile, optionally restricted to content-only hashing (excluding metadata). Used to verify profile identity across environments and ensure reproducible evaluation inputs.")
|
|
.Produces<RiskProfileHashResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/{profileId}/metadata", GetProfileMetadata)
|
|
.WithName("GetRiskProfileMetadata")
|
|
.WithSummary("Export risk profile metadata for notification enrichment (POLICY-RISK-40-002).")
|
|
.WithDescription("Export a compact metadata summary of a risk profile including signal names, severity thresholds, active version info, and custom metadata fields. Used by the notification enrichment pipeline to annotate policy-triggered alerts with human-readable risk context.")
|
|
.Produces<RiskProfileMetadataExportResponse>(StatusCodes.Status200OK)
|
|
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
private static IResult ListProfiles(
|
|
HttpContext context,
|
|
ITenantContextAccessor tenantAccessor,
|
|
RiskProfileConfigurationService profileService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var tenantId = tenantAccessor.TenantContext!.TenantId;
|
|
// POL-TEN-03: tenantId available for downstream scoping when repository layer is wired.
|
|
var ids = profileService.GetProfileIds();
|
|
var profiles = ids
|
|
.Select(id => profileService.GetProfile(id))
|
|
.Where(p => p != null)
|
|
.Select(p => new RiskProfileSummary(p!.Id, p.Version, p.Description))
|
|
.ToList();
|
|
|
|
return Results.Ok(new RiskProfileListResponse(profiles));
|
|
}
|
|
|
|
private static IResult GetProfile(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
ITenantContextAccessor tenantAccessor,
|
|
RiskProfileConfigurationService profileService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var tenantId = tenantAccessor.TenantContext!.TenantId;
|
|
var profile = profileService.GetProfile(profileId);
|
|
if (profile == null)
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = $"Risk profile '{profileId}' was not found.",
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
var hash = profileService.ComputeHash(profile);
|
|
return Results.Ok(new RiskProfileResponse(profile, hash));
|
|
}
|
|
|
|
private static IResult ListVersions(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var versions = lifecycleService.GetAllVersions(profileId);
|
|
return Results.Ok(new RiskProfileVersionListResponse(profileId, versions));
|
|
}
|
|
|
|
private static IResult GetVersion(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
[FromRoute] string version,
|
|
RiskProfileConfigurationService profileService,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var versionInfo = lifecycleService.GetVersionInfo(profileId, version);
|
|
if (versionInfo == null)
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Version not found",
|
|
Detail = $"Risk profile '{profileId}' version '{version}' was not found.",
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
var profile = profileService.GetProfile(profileId);
|
|
if (profile == null || profile.Version != version)
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = $"Risk profile '{profileId}' version '{version}' content not found.",
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
var hash = profileService.ComputeHash(profile);
|
|
return Results.Ok(new RiskProfileResponse(profile, hash, versionInfo));
|
|
}
|
|
|
|
private static IResult CreateProfile(
|
|
HttpContext context,
|
|
[FromBody] CreateRiskProfileRequest request,
|
|
ITenantContextAccessor tenantAccessor,
|
|
RiskProfileConfigurationService profileService,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var tenantId = tenantAccessor.TenantContext!.TenantId;
|
|
|
|
if (request?.Profile == null)
|
|
{
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Invalid request",
|
|
Detail = "Profile definition is required.",
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
|
|
var actorId = ResolveActorId(context);
|
|
|
|
try
|
|
{
|
|
var profile = request.Profile;
|
|
profileService.RegisterProfile(profile);
|
|
|
|
var versionInfo = lifecycleService.CreateVersion(profile, actorId);
|
|
var hash = profileService.ComputeHash(profile);
|
|
|
|
return Results.Created(
|
|
$"/api/risk/profiles/{profile.Id}/versions/{profile.Version}",
|
|
new RiskProfileResponse(profile, hash, versionInfo));
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Profile creation failed",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IResult ActivateProfile(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
[FromRoute] string version,
|
|
ITenantContextAccessor tenantAccessor,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var tenantId = tenantAccessor.TenantContext!.TenantId;
|
|
var actorId = ResolveActorId(context);
|
|
|
|
try
|
|
{
|
|
var versionInfo = lifecycleService.Activate(profileId, version, actorId);
|
|
return Results.Ok(new RiskProfileVersionInfoResponse(versionInfo));
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
if (ex.Message.Contains("not found"))
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Activation failed",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IResult DeprecateProfile(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
[FromRoute] string version,
|
|
[FromBody] DeprecateRiskProfileRequest? request,
|
|
ITenantContextAccessor tenantAccessor,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var tenantId = tenantAccessor.TenantContext!.TenantId;
|
|
var actorId = ResolveActorId(context);
|
|
|
|
try
|
|
{
|
|
var versionInfo = lifecycleService.Deprecate(
|
|
profileId,
|
|
version,
|
|
request?.SuccessorVersion,
|
|
request?.Reason,
|
|
actorId);
|
|
|
|
return Results.Ok(new RiskProfileVersionInfoResponse(versionInfo));
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
if (ex.Message.Contains("not found"))
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Deprecation failed",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IResult ArchiveProfile(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
[FromRoute] string version,
|
|
ITenantContextAccessor tenantAccessor,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var tenantId = tenantAccessor.TenantContext!.TenantId;
|
|
var actorId = ResolveActorId(context);
|
|
|
|
try
|
|
{
|
|
var versionInfo = lifecycleService.Archive(profileId, version, actorId);
|
|
return Results.Ok(new RiskProfileVersionInfoResponse(versionInfo));
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IResult GetProfileEvents(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
[FromQuery] int limit,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var effectiveLimit = limit > 0 ? limit : 100;
|
|
var events = lifecycleService.GetEvents(profileId, effectiveLimit);
|
|
return Results.Ok(new RiskProfileEventListResponse(profileId, events));
|
|
}
|
|
|
|
private static IResult CompareProfiles(
|
|
HttpContext context,
|
|
[FromBody] CompareRiskProfilesRequest request,
|
|
RiskProfileConfigurationService profileService,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
if (request == null ||
|
|
string.IsNullOrWhiteSpace(request.FromProfileId) ||
|
|
string.IsNullOrWhiteSpace(request.FromVersion) ||
|
|
string.IsNullOrWhiteSpace(request.ToProfileId) ||
|
|
string.IsNullOrWhiteSpace(request.ToVersion))
|
|
{
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Invalid request",
|
|
Detail = "Both from and to profile IDs and versions are required.",
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
|
|
var fromProfile = profileService.GetProfile(request.FromProfileId);
|
|
var toProfile = profileService.GetProfile(request.ToProfileId);
|
|
|
|
if (fromProfile == null)
|
|
{
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = $"From profile '{request.FromProfileId}' was not found.",
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
|
|
if (toProfile == null)
|
|
{
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = $"To profile '{request.ToProfileId}' was not found.",
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
var comparison = lifecycleService.CompareVersions(fromProfile, toProfile);
|
|
return Results.Ok(new RiskProfileComparisonResponse(comparison));
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return Results.BadRequest(new ProblemDetails
|
|
{
|
|
Title = "Comparison failed",
|
|
Detail = ex.Message,
|
|
Status = StatusCodes.Status400BadRequest
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IResult GetProfileHash(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
[FromQuery] bool contentOnly,
|
|
RiskProfileConfigurationService profileService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var profile = profileService.GetProfile(profileId);
|
|
if (profile == null)
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = $"Risk profile '{profileId}' was not found.",
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
var hash = contentOnly
|
|
? profileService.ComputeContentHash(profile)
|
|
: profileService.ComputeHash(profile);
|
|
|
|
return Results.Ok(new RiskProfileHashResponse(profile.Id, profile.Version, hash, contentOnly));
|
|
}
|
|
|
|
private static IResult GetProfileMetadata(
|
|
HttpContext context,
|
|
[FromRoute] string profileId,
|
|
RiskProfileConfigurationService profileService,
|
|
RiskProfileLifecycleService lifecycleService)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var profile = profileService.GetProfile(profileId);
|
|
if (profile == null)
|
|
{
|
|
return Results.NotFound(new ProblemDetails
|
|
{
|
|
Title = "Profile not found",
|
|
Detail = $"Risk profile '{profileId}' was not found.",
|
|
Status = StatusCodes.Status404NotFound
|
|
});
|
|
}
|
|
|
|
var versions = lifecycleService.GetAllVersions(profileId);
|
|
var activeVersion = versions.FirstOrDefault(v => v.Status == RiskProfileLifecycleStatus.Active);
|
|
var hash = profileService.ComputeHash(profile);
|
|
|
|
// Extract signal names and severity thresholds for notification context
|
|
var signalNames = profile.Signals.Select(s => s.Name).ToList();
|
|
var severityThresholds = profile.Overrides.Severity
|
|
.Select(s => new SeverityThresholdInfo(s.Set.ToString(), s.When))
|
|
.ToList();
|
|
|
|
return Results.Ok(new RiskProfileMetadataExportResponse(
|
|
ProfileId: profile.Id,
|
|
Version: profile.Version,
|
|
Description: profile.Description,
|
|
Hash: hash,
|
|
Status: activeVersion?.Status.ToString() ?? "unknown",
|
|
SignalNames: signalNames,
|
|
SeverityThresholds: severityThresholds,
|
|
CustomMetadata: profile.Metadata,
|
|
ExtendsProfile: profile.Extends,
|
|
ExportedAt: DateTime.UtcNow
|
|
));
|
|
}
|
|
|
|
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 Request/Response DTOs
|
|
|
|
internal sealed record RiskProfileListResponse(IReadOnlyList<RiskProfileSummary> Profiles);
|
|
|
|
internal sealed record RiskProfileSummary(string ProfileId, string Version, string? Description);
|
|
|
|
internal sealed record RiskProfileResponse(
|
|
RiskProfileModel Profile,
|
|
string Hash,
|
|
RiskProfileVersionInfo? VersionInfo = null);
|
|
|
|
internal sealed record RiskProfileVersionListResponse(
|
|
string ProfileId,
|
|
IReadOnlyList<RiskProfileVersionInfo> Versions);
|
|
|
|
internal sealed record RiskProfileVersionInfoResponse(RiskProfileVersionInfo VersionInfo);
|
|
|
|
internal sealed record RiskProfileEventListResponse(
|
|
string ProfileId,
|
|
IReadOnlyList<RiskProfileLifecycleEvent> Events);
|
|
|
|
internal sealed record RiskProfileComparisonResponse(RiskProfileVersionComparison Comparison);
|
|
|
|
internal sealed record RiskProfileHashResponse(
|
|
string ProfileId,
|
|
string Version,
|
|
string Hash,
|
|
bool ContentOnly);
|
|
|
|
internal sealed record CreateRiskProfileRequest(RiskProfileModel Profile);
|
|
|
|
internal sealed record DeprecateRiskProfileRequest(string? SuccessorVersion, string? Reason);
|
|
|
|
internal sealed record CompareRiskProfilesRequest(
|
|
string FromProfileId,
|
|
string FromVersion,
|
|
string ToProfileId,
|
|
string ToVersion);
|
|
|
|
/// <summary>
|
|
/// Metadata export response for notification enrichment (POLICY-RISK-40-002).
|
|
/// </summary>
|
|
internal sealed record RiskProfileMetadataExportResponse(
|
|
string ProfileId,
|
|
string Version,
|
|
string? Description,
|
|
string Hash,
|
|
string Status,
|
|
IReadOnlyList<string> SignalNames,
|
|
IReadOnlyList<SeverityThresholdInfo> SeverityThresholds,
|
|
Dictionary<string, object?>? CustomMetadata,
|
|
string? ExtendsProfile,
|
|
DateTime ExportedAt);
|
|
|
|
/// <summary>
|
|
/// Severity threshold information for notification context.
|
|
/// </summary>
|
|
internal sealed record SeverityThresholdInfo(
|
|
string TargetSeverity,
|
|
Dictionary<string, object> WhenConditions);
|
|
|
|
#endregion
|