// 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; /// /// Policy governance API endpoints for sealed mode and risk profile management. /// public static class GovernanceEndpoints { // In-memory stores for development private static readonly ConcurrentDictionary SealedModeStates = new(); private static readonly ConcurrentDictionary Overrides = new(); private static readonly ConcurrentDictionary RiskProfiles = new(); private static readonly ConcurrentDictionary AuditEntries = new(); /// /// Maps governance endpoints to the application. /// 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 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 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 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 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 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 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 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 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 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 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 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 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 ValidateRiskProfileAsync( HttpContext httpContext, ValidateRiskProfileRequest request) { var errors = new List(); var warnings = new List(); 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 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 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 TrustRoots { get; set; } = []; public List 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 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 Signals { get; init; } public required List SeverityOverrides { get; init; } public required List 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? TrustRoots { get; init; } public List? 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 TrustRoots { get; init; } public required List AllowedSources { get; init; } public required List 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 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? Signals { get; init; } public List? SeverityOverrides { get; init; } public List? ActionOverrides { get; init; } } public sealed record UpdateRiskProfileRequest { public string? Name { get; init; } public string? Description { get; init; } public List? Signals { get; init; } public List? SeverityOverrides { get; init; } public List? ActionOverrides { get; init; } } public sealed record DeprecateProfileRequest { public required string Reason { get; init; } } public sealed record ValidateRiskProfileRequest { public string? Name { get; init; } public List? 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 Signals { get; init; } public required List SeverityOverrides { get; init; } public required List 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 Errors { get; init; } public required List 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 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; } }