feat(audit): resource enrichers + before-state providers for critical services

- Authority: resolve user/client/role/tenant GUIDs to names, capture before-state
- Policy: resolve exception/pack/profile GUIDs, capture governance state
- Release-Orchestrator: resolve release GUIDs to name+version
- Findings: resolve finding GUIDs to CVE+package
- All enrichers fire-and-forget with graceful fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-09 12:30:49 +03:00
parent 92c2a8591c
commit c698ff40cc
10 changed files with 750 additions and 24 deletions

View File

@@ -0,0 +1,142 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.Extensions.Logging;
using StellaOps.Audit.Emission;
using StellaOps.Policy.Engine.AirGap;
using StellaOps.Policy.Persistence.Postgres.Repositories;
namespace StellaOps.Policy.Engine.Audit;
/// <summary>
/// Captures the "before" state of Policy resources (governance settings, exceptions, risk profiles)
/// prior to mutation, enabling before/after diffs in audit events.
/// </summary>
internal sealed class PolicyAuditBeforeStateProvider : IAuditBeforeStateProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly ISealedModeService _sealedModeService;
private readonly ILogger<PolicyAuditBeforeStateProvider> _logger;
public PolicyAuditBeforeStateProvider(
IServiceProvider serviceProvider,
ISealedModeService sealedModeService,
ILogger<PolicyAuditBeforeStateProvider> logger)
{
_serviceProvider = serviceProvider;
_sealedModeService = sealedModeService;
_logger = logger;
}
public string Module => AuditModules.Policy;
public async ValueTask<Dictionary<string, object?>?> GetBeforeStateAsync(
string resourceType,
string resourceId,
CancellationToken ct)
{
try
{
return resourceType.ToLowerInvariant() switch
{
"governance" or "sealed_mode" or "sealed-mode" =>
await GetGovernanceStateAsync(resourceId, ct).ConfigureAwait(false),
"exception" or "exceptions" =>
await GetExceptionStateAsync(resourceId, ct).ConfigureAwait(false),
"risk_profile" or "risk-profile" or "riskprofile" or "profile" =>
await GetRiskProfileStateAsync(resourceId, ct).ConfigureAwait(false),
_ => null
};
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Policy before-state capture failed for {ResourceType}/{ResourceId}",
resourceType, resourceId);
return null;
}
}
private async Task<Dictionary<string, object?>?> GetGovernanceStateAsync(
string resourceId, CancellationToken ct)
{
// resourceId may be a tenantId or "default"
var tenantId = string.IsNullOrWhiteSpace(resourceId) ? "default" : resourceId;
try
{
var state = await _sealedModeService.GetStateAsync(tenantId, ct).ConfigureAwait(false);
return new Dictionary<string, object?>
{
["sealedMode"] = state.IsSealed,
["enforcementLevel"] = state.IsSealed ? "sealed" : "open"
};
}
catch
{
// Sealed mode may not be initialized for this tenant
return new Dictionary<string, object?>
{
["sealedMode"] = false,
["enforcementLevel"] = "open"
};
}
}
private async Task<Dictionary<string, object?>?> GetExceptionStateAsync(
string resourceId, CancellationToken ct)
{
if (!Guid.TryParse(resourceId, out var exceptionId))
{
return null;
}
using var scope = _serviceProvider.CreateScope();
var repo = scope.ServiceProvider.GetService<IExceptionRepository>();
if (repo is null)
{
return null;
}
var exception = await repo.GetByIdAsync("default", exceptionId, ct).ConfigureAwait(false);
if (exception is null)
{
return null;
}
return new Dictionary<string, object?>
{
["status"] = exception.Status.ToString(),
["scope"] = exception.RulePattern ?? exception.ResourcePattern ?? exception.ArtifactPattern,
["expiry"] = exception.ExpiresAt?.ToString("O")
};
}
private async Task<Dictionary<string, object?>?> GetRiskProfileStateAsync(
string resourceId, CancellationToken ct)
{
if (!Guid.TryParse(resourceId, out var profileId))
{
return null;
}
using var scope = _serviceProvider.CreateScope();
var repo = scope.ServiceProvider.GetService<IRiskProfileRepository>();
if (repo is null)
{
return null;
}
var profile = await repo.GetByIdAsync("default", profileId, ct).ConfigureAwait(false);
if (profile is null)
{
return null;
}
return new Dictionary<string, object?>
{
["name"] = profile.DisplayName ?? profile.Name,
["thresholds"] = profile.Thresholds,
["active"] = profile.IsActive
};
}
}

View File

@@ -0,0 +1,130 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.Extensions.Logging;
using StellaOps.Audit.Emission;
using StellaOps.Policy.Persistence.Postgres.Repositories;
namespace StellaOps.Policy.Engine.Audit;
/// <summary>
/// Resolves Policy resource GUIDs (exception, policy pack, risk profile) to human-readable names
/// for audit event enrichment.
/// </summary>
internal sealed class PolicyAuditResourceEnricher : IAuditResourceEnricher
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PolicyAuditResourceEnricher> _logger;
public PolicyAuditResourceEnricher(
IServiceProvider serviceProvider,
ILogger<PolicyAuditResourceEnricher> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public string Module => AuditModules.Policy;
public async ValueTask<AuditResourcePayload> EnrichAsync(
string resourceType,
string resourceId,
CancellationToken ct)
{
var fallback = new AuditResourcePayload { Type = resourceType, Id = resourceId };
try
{
// Repositories are scoped; create a scope for resolution.
using var scope = _serviceProvider.CreateScope();
var name = resourceType.ToLowerInvariant() switch
{
"exception" or "exceptions" => await ResolveExceptionNameAsync(scope.ServiceProvider, resourceId, ct).ConfigureAwait(false),
"pack" or "packs" or "policy_pack" or "policy-pack" => await ResolvePackNameAsync(scope.ServiceProvider, resourceId, ct).ConfigureAwait(false),
"risk_profile" or "risk-profile" or "riskprofile" or "profile" => await ResolveRiskProfileNameAsync(scope.ServiceProvider, resourceId, ct).ConfigureAwait(false),
_ => null
};
return name is not null
? fallback with { Name = name }
: fallback;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Policy audit enrichment failed for {ResourceType}/{ResourceId}",
resourceType, resourceId);
return fallback;
}
}
private static async Task<string?> ResolveExceptionNameAsync(
IServiceProvider sp, string resourceId, CancellationToken ct)
{
if (!Guid.TryParse(resourceId, out _))
{
return null;
}
var repo = sp.GetService<IExceptionRepository>();
if (repo is null)
{
return null;
}
// Exception repository requires tenantId; try "default" and common tenant IDs.
// In the fire-and-forget context, we do a best-effort lookup.
var exception = await repo.GetByIdAsync("default", Guid.Parse(resourceId), ct).ConfigureAwait(false);
if (exception is not null)
{
return !string.IsNullOrWhiteSpace(exception.Name)
? $"{exception.Name} ({exception.Status})"
: exception.ExceptionId;
}
return null;
}
private static async Task<string?> ResolvePackNameAsync(
IServiceProvider sp, string resourceId, CancellationToken ct)
{
if (!Guid.TryParse(resourceId, out var packId))
{
return null;
}
var repo = sp.GetService<IPackRepository>();
if (repo is null)
{
return null;
}
var pack = await repo.GetByIdAsync("default", packId, ct).ConfigureAwait(false);
if (pack is not null)
{
var versionSuffix = pack.ActiveVersion.HasValue ? $" v{pack.ActiveVersion}" : "";
return $"{pack.DisplayName ?? pack.Name}{versionSuffix}";
}
return null;
}
private static async Task<string?> ResolveRiskProfileNameAsync(
IServiceProvider sp, string resourceId, CancellationToken ct)
{
if (!Guid.TryParse(resourceId, out var profileId))
{
return null;
}
var repo = sp.GetService<IRiskProfileRepository>();
if (repo is null)
{
return null;
}
var profile = await repo.GetByIdAsync("default", profileId, ct).ConfigureAwait(false);
return profile is not null
? profile.DisplayName ?? profile.Name
: null;
}
}

View File

@@ -423,6 +423,8 @@ builder.Services.AddMemoryCache();
// Unified audit emission (posts audit events to Timeline service)
builder.Services.AddAuditEmission(builder.Configuration);
builder.Services.AddSingleton<StellaOps.Audit.Emission.IAuditResourceEnricher, StellaOps.Policy.Engine.Audit.PolicyAuditResourceEnricher>();
builder.Services.AddSingleton<StellaOps.Audit.Emission.IAuditBeforeStateProvider, StellaOps.Policy.Engine.Audit.PolicyAuditBeforeStateProvider>();
// Stella Router integration
var routerEnabled = builder.Services.AddRouterMicroservice(