Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs

881 lines
32 KiB
C#

// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20251229_021a_FE_policy_governance_controls
// Task: GOV-018 - Sealed mode overrides and risk profile events endpoints
using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
namespace StellaOps.Policy.Gateway.Endpoints;
/// <summary>
/// Policy governance API endpoints for sealed mode and risk profile management.
/// </summary>
public static class GovernanceEndpoints
{
// In-memory stores for development
private static readonly ConcurrentDictionary<string, SealedModeState> SealedModeStates = new();
private static readonly ConcurrentDictionary<string, SealedModeOverrideEntity> Overrides = new();
private static readonly ConcurrentDictionary<string, RiskProfileEntity> RiskProfiles = new();
private static readonly ConcurrentDictionary<string, GovernanceAuditEntry> AuditEntries = new();
/// <summary>
/// Maps governance endpoints to the application.
/// </summary>
public static void MapGovernanceEndpoints(this WebApplication app)
{
var governance = app.MapGroup("/api/v1/governance")
.WithTags("Governance");
// Sealed Mode endpoints
governance.MapGet("/sealed-mode/status", GetSealedModeStatusAsync)
.WithName("GetSealedModeStatus")
.WithDescription("Get sealed mode status");
governance.MapGet("/sealed-mode/overrides", GetSealedModeOverridesAsync)
.WithName("GetSealedModeOverrides")
.WithDescription("List sealed mode overrides");
governance.MapPost("/sealed-mode/toggle", ToggleSealedModeAsync)
.WithName("ToggleSealedMode")
.WithDescription("Toggle sealed mode on/off");
governance.MapPost("/sealed-mode/overrides", CreateSealedModeOverrideAsync)
.WithName("CreateSealedModeOverride")
.WithDescription("Create a sealed mode override");
governance.MapPost("/sealed-mode/overrides/{overrideId}/revoke", RevokeSealedModeOverrideAsync)
.WithName("RevokeSealedModeOverride")
.WithDescription("Revoke a sealed mode override");
// Risk Profile endpoints
governance.MapGet("/risk-profiles", ListRiskProfilesAsync)
.WithName("ListRiskProfiles")
.WithDescription("List risk profiles");
governance.MapGet("/risk-profiles/{profileId}", GetRiskProfileAsync)
.WithName("GetRiskProfile")
.WithDescription("Get a risk profile by ID");
governance.MapPost("/risk-profiles", CreateRiskProfileAsync)
.WithName("CreateRiskProfile")
.WithDescription("Create a new risk profile");
governance.MapPut("/risk-profiles/{profileId}", UpdateRiskProfileAsync)
.WithName("UpdateRiskProfile")
.WithDescription("Update a risk profile");
governance.MapDelete("/risk-profiles/{profileId}", DeleteRiskProfileAsync)
.WithName("DeleteRiskProfile")
.WithDescription("Delete a risk profile");
governance.MapPost("/risk-profiles/{profileId}/activate", ActivateRiskProfileAsync)
.WithName("ActivateRiskProfile")
.WithDescription("Activate a risk profile");
governance.MapPost("/risk-profiles/{profileId}/deprecate", DeprecateRiskProfileAsync)
.WithName("DeprecateRiskProfile")
.WithDescription("Deprecate a risk profile");
governance.MapPost("/risk-profiles/validate", ValidateRiskProfileAsync)
.WithName("ValidateRiskProfile")
.WithDescription("Validate a risk profile");
// Audit endpoints
governance.MapGet("/audit/events", GetAuditEventsAsync)
.WithName("GetGovernanceAuditEvents")
.WithDescription("Get governance audit events");
governance.MapGet("/audit/events/{eventId}", GetAuditEventAsync)
.WithName("GetGovernanceAuditEvent")
.WithDescription("Get a specific audit event");
// Initialize default profiles
InitializeDefaultProfiles();
}
// ========================================================================
// Sealed Mode Handlers
// ========================================================================
private static Task<IResult> GetSealedModeStatusAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
[FromQuery] string? tenantId)
{
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
var response = new SealedModeStatusResponse
{
IsSealed = state.IsSealed,
SealedAt = state.SealedAt,
SealedBy = state.SealedBy,
Reason = state.Reason,
TrustRoots = state.TrustRoots,
AllowedSources = state.AllowedSources,
Overrides = Overrides.Values
.Where(o => o.TenantId == tenant && o.Active)
.Select(MapOverrideToResponse)
.ToList(),
VerificationStatus = "verified",
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
return Task.FromResult(Results.Ok(response));
}
private static Task<IResult> GetSealedModeOverridesAsync(
HttpContext httpContext,
[FromQuery] string? tenantId)
{
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
var overrides = Overrides.Values
.Where(o => o.TenantId == tenant)
.Select(MapOverrideToResponse)
.ToList();
return Task.FromResult(Results.Ok(overrides));
}
private static Task<IResult> ToggleSealedModeAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
SealedModeToggleRequest request)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = timeProvider.GetUtcNow();
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
if (request.Enable)
{
state = new SealedModeState
{
IsSealed = true,
SealedAt = now.ToString("O", CultureInfo.InvariantCulture),
SealedBy = actor,
Reason = request.Reason,
TrustRoots = request.TrustRoots ?? [],
AllowedSources = request.AllowedSources ?? []
};
}
else
{
state = new SealedModeState
{
IsSealed = false,
LastUnsealedAt = now.ToString("O", CultureInfo.InvariantCulture)
};
}
SealedModeStates[tenant] = state;
// Audit
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider);
var response = new SealedModeStatusResponse
{
IsSealed = state.IsSealed,
SealedAt = state.SealedAt,
SealedBy = state.SealedBy,
Reason = state.Reason,
TrustRoots = state.TrustRoots,
AllowedSources = state.AllowedSources,
Overrides = [],
VerificationStatus = "verified",
LastVerifiedAt = now.ToString("O", CultureInfo.InvariantCulture)
};
return Task.FromResult(Results.Ok(response));
}
private static Task<IResult> CreateSealedModeOverrideAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
SealedModeOverrideRequest request)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = timeProvider.GetUtcNow();
var overrideId = $"override-{Guid.NewGuid():N}";
var entity = new SealedModeOverrideEntity
{
Id = overrideId,
TenantId = tenant,
Type = request.Type,
Target = request.Target,
Reason = request.Reason,
ApprovalId = $"approval-{Guid.NewGuid():N}",
ApprovedBy = [actor],
ExpiresAt = now.AddHours(request.DurationHours).ToString("O", CultureInfo.InvariantCulture),
CreatedAt = now.ToString("O", CultureInfo.InvariantCulture),
Active = true
};
Overrides[overrideId] = entity;
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
$"Created override for {request.Target}: {request.Reason}", timeProvider);
return Task.FromResult(Results.Ok(MapOverrideToResponse(entity)));
}
private static Task<IResult> RevokeSealedModeOverrideAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
string overrideId,
RevokeOverrideRequest request)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
if (!Overrides.TryGetValue(overrideId, out var entity) || entity.TenantId != tenant)
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Override not found",
Status = 404
}));
}
entity.Active = false;
Overrides[overrideId] = entity;
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
$"Revoked override: {request.Reason}", timeProvider);
return Task.FromResult(Results.NoContent());
}
// ========================================================================
// Risk Profile Handlers
// ========================================================================
private static Task<IResult> ListRiskProfilesAsync(
HttpContext httpContext,
[FromQuery] string? tenantId,
[FromQuery] string? status)
{
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
var profiles = RiskProfiles.Values
.Where(p => p.TenantId == tenant || p.TenantId == "default")
.Where(p => string.IsNullOrEmpty(status) || p.Status.Equals(status, StringComparison.OrdinalIgnoreCase))
.Select(MapProfileToResponse)
.ToList();
return Task.FromResult(Results.Ok(profiles));
}
private static Task<IResult> GetRiskProfileAsync(
HttpContext httpContext,
string profileId)
{
var tenant = GetTenantId(httpContext) ?? "default";
if (!RiskProfiles.TryGetValue(profileId, out var profile))
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Profile not found",
Status = 404,
Detail = $"Risk profile '{profileId}' not found."
}));
}
return Task.FromResult(Results.Ok(MapProfileToResponse(profile)));
}
private static Task<IResult> CreateRiskProfileAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
CreateRiskProfileRequest request)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = timeProvider.GetUtcNow();
var profileId = $"profile-{Guid.NewGuid():N}";
var entity = new RiskProfileEntity
{
Id = profileId,
TenantId = tenant,
Version = "1.0.0",
Name = request.Name,
Description = request.Description,
Status = "draft",
ExtendsProfile = request.ExtendsProfile,
Signals = request.Signals ?? [],
SeverityOverrides = request.SeverityOverrides ?? [],
ActionOverrides = request.ActionOverrides ?? [],
CreatedAt = now.ToString("O", CultureInfo.InvariantCulture),
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
CreatedBy = actor,
ModifiedBy = actor
};
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
$"Created risk profile: {request.Name}", timeProvider);
return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity)));
}
private static Task<IResult> UpdateRiskProfileAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
string profileId,
UpdateRiskProfileRequest request)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = timeProvider.GetUtcNow();
if (!RiskProfiles.TryGetValue(profileId, out var existing))
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Profile not found",
Status = 404
}));
}
var entity = existing with
{
Name = request.Name ?? existing.Name,
Description = request.Description ?? existing.Description,
Signals = request.Signals ?? existing.Signals,
SeverityOverrides = request.SeverityOverrides ?? existing.SeverityOverrides,
ActionOverrides = request.ActionOverrides ?? existing.ActionOverrides,
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
ModifiedBy = actor
};
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
$"Updated risk profile: {entity.Name}", timeProvider);
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
}
private static Task<IResult> DeleteRiskProfileAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
string profileId)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
if (!RiskProfiles.TryRemove(profileId, out var removed))
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Profile not found",
Status = 404
}));
}
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
$"Deleted risk profile: {removed.Name}", timeProvider);
return Task.FromResult(Results.NoContent());
}
private static Task<IResult> ActivateRiskProfileAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
string profileId)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = timeProvider.GetUtcNow();
if (!RiskProfiles.TryGetValue(profileId, out var existing))
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Profile not found",
Status = 404
}));
}
var entity = existing with
{
Status = "active",
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
ModifiedBy = actor
};
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
$"Activated risk profile: {entity.Name}", timeProvider);
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
}
private static Task<IResult> DeprecateRiskProfileAsync(
HttpContext httpContext,
[FromServices] TimeProvider timeProvider,
string profileId,
DeprecateProfileRequest request)
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = timeProvider.GetUtcNow();
if (!RiskProfiles.TryGetValue(profileId, out var existing))
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Profile not found",
Status = 404
}));
}
var entity = existing with
{
Status = "deprecated",
ModifiedAt = now.ToString("O", CultureInfo.InvariantCulture),
ModifiedBy = actor,
DeprecationReason = request.Reason
};
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider);
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
}
private static Task<IResult> ValidateRiskProfileAsync(
HttpContext httpContext,
ValidateRiskProfileRequest request)
{
var errors = new List<ValidationError>();
var warnings = new List<ValidationWarning>();
if (string.IsNullOrWhiteSpace(request.Name))
{
errors.Add(new ValidationError("MISSING_NAME", "Profile name is required", "name"));
}
if (request.Signals == null || request.Signals.Count == 0)
{
errors.Add(new ValidationError("NO_SIGNALS", "At least one signal must be defined", "signals"));
}
else
{
var totalWeight = request.Signals.Where(s => s.Enabled).Sum(s => s.Weight);
if (Math.Abs(totalWeight - 1.0) > 0.01)
{
warnings.Add(new ValidationWarning("WEIGHT_SUM", $"Signal weights sum to {totalWeight:F2}, expected 1.0", "signals"));
}
}
var response = new RiskProfileValidationResponse
{
Valid = errors.Count == 0,
Errors = errors,
Warnings = warnings
};
return Task.FromResult(Results.Ok(response));
}
// ========================================================================
// Audit Handlers
// ========================================================================
private static Task<IResult> GetAuditEventsAsync(
HttpContext httpContext,
[FromQuery] string? tenantId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
var events = AuditEntries.Values
.Where(e => e.TenantId == tenant)
.OrderByDescending(e => e.Timestamp)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(MapAuditToResponse)
.ToList();
var total = AuditEntries.Values.Count(e => e.TenantId == tenant);
var response = new AuditEventsResponse
{
Events = events,
Total = total,
Page = page,
PageSize = pageSize,
HasMore = (page * pageSize) < total
};
return Task.FromResult(Results.Ok(response));
}
private static Task<IResult> GetAuditEventAsync(
HttpContext httpContext,
string eventId)
{
var tenant = GetTenantId(httpContext) ?? "default";
if (!AuditEntries.TryGetValue(eventId, out var entry) || entry.TenantId != tenant)
{
return Task.FromResult(Results.NotFound(new ProblemDetails
{
Title = "Event not found",
Status = 404
}));
}
return Task.FromResult(Results.Ok(MapAuditToResponse(entry)));
}
// ========================================================================
// Helper Methods
// ========================================================================
private static void InitializeDefaultProfiles()
{
if (RiskProfiles.IsEmpty)
{
var now = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture);
RiskProfiles["profile-default"] = new RiskProfileEntity
{
Id = "profile-default",
TenantId = "default",
Version = "1.0.0",
Name = "Default Risk Profile",
Description = "Standard risk evaluation profile",
Status = "active",
Signals = [
new RiskSignal { Name = "cvss_score", Weight = 0.3, Description = "CVSS base score", Enabled = true },
new RiskSignal { Name = "exploit_available", Weight = 0.25, Description = "Known exploit exists", Enabled = true },
new RiskSignal { Name = "reachability", Weight = 0.2, Description = "Code reachability", Enabled = true },
new RiskSignal { Name = "asset_criticality", Weight = 0.15, Description = "Asset business criticality", Enabled = true },
new RiskSignal { Name = "patch_available", Weight = 0.1, Description = "Patch availability", Enabled = true }
],
SeverityOverrides = [],
ActionOverrides = [],
CreatedAt = now,
ModifiedAt = now,
CreatedBy = "system",
ModifiedBy = "system"
};
}
}
private static string? GetTenantId(HttpContext httpContext)
{
return httpContext.User.Claims.FirstOrDefault(c => c.Type == "tenant_id")?.Value
?? httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault()
?? httpContext.Request.Headers["X-StellaOps-Tenant"].FirstOrDefault();
}
private static string? GetActorId(HttpContext httpContext)
{
return httpContext.User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value
?? httpContext.User.Identity?.Name
?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault();
}
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider)
{
var id = $"audit-{Guid.NewGuid():N}";
AuditEntries[id] = new GovernanceAuditEntry
{
Id = id,
TenantId = tenantId,
Type = eventType,
Timestamp = timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
Actor = actor,
ActorType = "user",
TargetResource = targetId,
TargetResourceType = targetType,
Summary = summary
};
}
private static SealedModeOverrideResponse MapOverrideToResponse(SealedModeOverrideEntity entity)
{
return new SealedModeOverrideResponse
{
Id = entity.Id,
Type = entity.Type,
Target = entity.Target,
Reason = entity.Reason,
ApprovalId = entity.ApprovalId,
ApprovedBy = entity.ApprovedBy,
ExpiresAt = entity.ExpiresAt,
CreatedAt = entity.CreatedAt,
Active = entity.Active
};
}
private static RiskProfileResponse MapProfileToResponse(RiskProfileEntity entity)
{
return new RiskProfileResponse
{
Id = entity.Id,
Version = entity.Version,
Name = entity.Name,
Description = entity.Description,
Status = entity.Status,
ExtendsProfile = entity.ExtendsProfile,
Signals = entity.Signals,
SeverityOverrides = entity.SeverityOverrides,
ActionOverrides = entity.ActionOverrides,
CreatedAt = entity.CreatedAt,
ModifiedAt = entity.ModifiedAt,
CreatedBy = entity.CreatedBy,
ModifiedBy = entity.ModifiedBy
};
}
private static GovernanceAuditEventResponse MapAuditToResponse(GovernanceAuditEntry entry)
{
return new GovernanceAuditEventResponse
{
Id = entry.Id,
Type = entry.Type,
Timestamp = entry.Timestamp,
Actor = entry.Actor,
ActorType = entry.ActorType,
TargetResource = entry.TargetResource,
TargetResourceType = entry.TargetResourceType,
Summary = entry.Summary,
TenantId = entry.TenantId
};
}
}
// ============================================================================
// Internal Entities
// ============================================================================
internal sealed class SealedModeState
{
public bool IsSealed { get; set; }
public string? SealedAt { get; set; }
public string? SealedBy { get; set; }
public string? Reason { get; set; }
public string? LastUnsealedAt { get; set; }
public List<string> TrustRoots { get; set; } = [];
public List<string> AllowedSources { get; set; } = [];
}
internal sealed record SealedModeOverrideEntity
{
public required string Id { get; init; }
public required string TenantId { get; init; }
public required string Type { get; init; }
public required string Target { get; init; }
public required string Reason { get; init; }
public required string ApprovalId { get; init; }
public required List<string> ApprovedBy { get; init; }
public required string ExpiresAt { get; init; }
public required string CreatedAt { get; init; }
public bool Active { get; set; }
}
internal sealed record RiskProfileEntity
{
public required string Id { get; init; }
public required string TenantId { get; init; }
public required string Version { get; init; }
public required string Name { get; init; }
public string? Description { get; init; }
public required string Status { get; init; }
public string? ExtendsProfile { get; init; }
public required List<RiskSignal> Signals { get; init; }
public required List<SeverityOverride> SeverityOverrides { get; init; }
public required List<ActionOverride> ActionOverrides { get; init; }
public required string CreatedAt { get; init; }
public required string ModifiedAt { get; init; }
public required string CreatedBy { get; init; }
public required string ModifiedBy { get; init; }
public string? DeprecationReason { get; init; }
}
internal sealed record GovernanceAuditEntry
{
public required string Id { get; init; }
public required string TenantId { get; init; }
public required string Type { get; init; }
public required string Timestamp { get; init; }
public required string Actor { get; init; }
public required string ActorType { get; init; }
public required string TargetResource { get; init; }
public required string TargetResourceType { get; init; }
public required string Summary { get; init; }
}
// ============================================================================
// Request/Response DTOs
// ============================================================================
public sealed record SealedModeToggleRequest
{
public required bool Enable { get; init; }
public string? Reason { get; init; }
public List<string>? TrustRoots { get; init; }
public List<string>? AllowedSources { get; init; }
}
public sealed record SealedModeOverrideRequest
{
public required string Type { get; init; }
public required string Target { get; init; }
public required string Reason { get; init; }
public int DurationHours { get; init; } = 24;
}
public sealed record RevokeOverrideRequest
{
public required string Reason { get; init; }
}
public sealed record SealedModeStatusResponse
{
public required bool IsSealed { get; init; }
public string? SealedAt { get; init; }
public string? SealedBy { get; init; }
public string? Reason { get; init; }
public required List<string> TrustRoots { get; init; }
public required List<string> AllowedSources { get; init; }
public required List<SealedModeOverrideResponse> Overrides { get; init; }
public required string VerificationStatus { get; init; }
public string? LastVerifiedAt { get; init; }
}
public sealed record SealedModeOverrideResponse
{
public required string Id { get; init; }
public required string Type { get; init; }
public required string Target { get; init; }
public required string Reason { get; init; }
public required string ApprovalId { get; init; }
public required List<string> ApprovedBy { get; init; }
public required string ExpiresAt { get; init; }
public required string CreatedAt { get; init; }
public required bool Active { get; init; }
}
public sealed record CreateRiskProfileRequest
{
public required string Name { get; init; }
public string? Description { get; init; }
public string? ExtendsProfile { get; init; }
public List<RiskSignal>? Signals { get; init; }
public List<SeverityOverride>? SeverityOverrides { get; init; }
public List<ActionOverride>? ActionOverrides { get; init; }
}
public sealed record UpdateRiskProfileRequest
{
public string? Name { get; init; }
public string? Description { get; init; }
public List<RiskSignal>? Signals { get; init; }
public List<SeverityOverride>? SeverityOverrides { get; init; }
public List<ActionOverride>? ActionOverrides { get; init; }
}
public sealed record DeprecateProfileRequest
{
public required string Reason { get; init; }
}
public sealed record ValidateRiskProfileRequest
{
public string? Name { get; init; }
public List<RiskSignal>? Signals { get; init; }
}
public sealed record RiskProfileResponse
{
public required string Id { get; init; }
public required string Version { get; init; }
public required string Name { get; init; }
public string? Description { get; init; }
public required string Status { get; init; }
public string? ExtendsProfile { get; init; }
public required List<RiskSignal> Signals { get; init; }
public required List<SeverityOverride> SeverityOverrides { get; init; }
public required List<ActionOverride> ActionOverrides { get; init; }
public required string CreatedAt { get; init; }
public required string ModifiedAt { get; init; }
public required string CreatedBy { get; init; }
public required string ModifiedBy { get; init; }
}
public sealed record RiskProfileValidationResponse
{
public required bool Valid { get; init; }
public required List<ValidationError> Errors { get; init; }
public required List<ValidationWarning> Warnings { get; init; }
}
public sealed record ValidationError(string Code, string Message, string? Path = null);
public sealed record ValidationWarning(string Code, string Message, string? Path = null);
public sealed record RiskSignal
{
public required string Name { get; init; }
public required double Weight { get; init; }
public string? Description { get; init; }
public required bool Enabled { get; init; }
}
public sealed record SeverityOverride
{
public string? Id { get; init; }
public string? TargetSeverity { get; init; }
public object? Condition { get; init; }
public string? Description { get; init; }
public int Priority { get; init; }
}
public sealed record ActionOverride
{
public string? Id { get; init; }
public string? TargetAction { get; init; }
public object? Condition { get; init; }
public string? Description { get; init; }
public int Priority { get; init; }
}
public sealed record AuditEventsResponse
{
public required List<GovernanceAuditEventResponse> Events { get; init; }
public required int Total { get; init; }
public required int Page { get; init; }
public required int PageSize { get; init; }
public required bool HasMore { get; init; }
}
public sealed record GovernanceAuditEventResponse
{
public required string Id { get; init; }
public required string Type { get; init; }
public required string Timestamp { get; init; }
public required string Actor { get; init; }
public required string ActorType { get; init; }
public required string TargetResource { get; init; }
public required string TargetResourceType { get; init; }
public required string Summary { get; init; }
public required string TenantId { get; init; }
}