881 lines
32 KiB
C#
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; }
|
|
}
|