feat(audit): enhanced AuditActionFilter with body capture + enrichment hooks
- Capture request body (JSON, up to 64KB, PII-redacted) in Details["requestBody"] - Capture response resource ID for create operations in Details["responseResourceId"] - Add IAuditResourceEnricher interface for GUID -> human-readable name resolution - Add IAuditBeforeStateProvider for before-state snapshots in Details["beforeState"] - Add AuditPiiRedactor with configurable field patterns (recursive JSON walk) - AuditActionAttribute gains CaptureBody (bool?) + SensitiveFields (string[]?) - AuditEmissionOptions gains MaxBodySizeBytes (64KB) + RedactedFieldPatterns - All enrichment is optional and fire-and-forget (never blocks response) - Add AuditModules constants (15 modules) and AuditActions constants (~200 actions) organized as nested static classes per module for type-safe annotations - All 17 consuming services verified to compile successfully Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,20 @@ public sealed class AuditActionAttribute : Attribute
|
||||
/// </summary>
|
||||
public string? ResourceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the request body should be captured in the audit event's <c>Details["requestBody"]</c>.
|
||||
/// Default is <c>true</c> for POST/PUT/PATCH and <c>false</c> for all other methods.
|
||||
/// Set explicitly to override the default behavior.
|
||||
/// </summary>
|
||||
public bool? CaptureBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional field names to redact from the request body, beyond the defaults
|
||||
/// in <see cref="AuditPiiRedactor.DefaultRedactedPatterns"/>.
|
||||
/// Matching is case-insensitive and strips separators (underscores, hyphens).
|
||||
/// </summary>
|
||||
public string[]? SensitiveFields { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new audit action attribute.
|
||||
/// </summary>
|
||||
@@ -49,4 +63,19 @@ public sealed class AuditActionAttribute : Attribute
|
||||
Module = module;
|
||||
Action = action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves whether the body should be captured for the given HTTP method.
|
||||
/// If <see cref="CaptureBody"/> is explicitly set, that value wins.
|
||||
/// Otherwise, defaults to <c>true</c> for POST/PUT/PATCH and <c>false</c> for everything else.
|
||||
/// </summary>
|
||||
internal bool ShouldCaptureBody(string httpMethod)
|
||||
{
|
||||
if (CaptureBody.HasValue)
|
||||
{
|
||||
return CaptureBody.Value;
|
||||
}
|
||||
|
||||
return httpMethod is "POST" or "PUT" or "PATCH";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
@@ -14,6 +18,15 @@ namespace StellaOps.Audit.Emission;
|
||||
/// The filter reads <see cref="AuditActionAttribute"/> metadata from the endpoint.
|
||||
/// If the attribute is not present, the filter is a no-op passthrough.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Enhanced capabilities:
|
||||
/// <list type="bullet">
|
||||
/// <item>Request body capture (JSON, up to configurable max size, PII-redacted)</item>
|
||||
/// <item>Response resource ID extraction for create operations</item>
|
||||
/// <item>Before-state capture via <see cref="IAuditBeforeStateProvider"/></item>
|
||||
/// <item>Resource name enrichment via <see cref="IAuditResourceEnricher"/></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usage in minimal API registration:
|
||||
@@ -27,41 +40,166 @@ public sealed class AuditActionFilter : IEndpointFilter
|
||||
{
|
||||
private readonly IAuditEventEmitter _emitter;
|
||||
private readonly ILogger<AuditActionFilter> _logger;
|
||||
private readonly IOptions<AuditEmissionOptions> _options;
|
||||
|
||||
public AuditActionFilter(IAuditEventEmitter emitter, ILogger<AuditActionFilter> logger)
|
||||
public AuditActionFilter(
|
||||
IAuditEventEmitter emitter,
|
||||
ILogger<AuditActionFilter> logger,
|
||||
IOptions<AuditEmissionOptions> options)
|
||||
{
|
||||
_emitter = emitter;
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public async ValueTask<object?> InvokeAsync(
|
||||
EndpointFilterInvocationContext context,
|
||||
EndpointFilterDelegate next)
|
||||
{
|
||||
// Execute the endpoint first
|
||||
var result = await next(context).ConfigureAwait(false);
|
||||
|
||||
// Check for the audit attribute
|
||||
// Check for the audit attribute early so we can decide whether to capture pre-state
|
||||
var auditAttr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<AuditActionAttribute>();
|
||||
if (auditAttr is null)
|
||||
{
|
||||
return result;
|
||||
return await next(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
var options = _options.Value;
|
||||
|
||||
// --- Pre-execution captures ---
|
||||
|
||||
// 1. Capture request body (if applicable)
|
||||
JsonNode? capturedBody = null;
|
||||
if (auditAttr.ShouldCaptureBody(httpContext.Request.Method))
|
||||
{
|
||||
capturedBody = await CaptureRequestBodyAsync(httpContext, auditAttr, options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 2. Capture before-state (if a provider is registered)
|
||||
Dictionary<string, object?>? beforeState = null;
|
||||
var resourceId = ResolveResourceId(httpContext);
|
||||
var resourceType = auditAttr.ResourceType ?? InferResourceType(httpContext.Request.Path.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resourceId))
|
||||
{
|
||||
beforeState = await CaptureBeforeStateAsync(httpContext, auditAttr, resourceType, resourceId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// --- Execute the endpoint ---
|
||||
var result = await next(context).ConfigureAwait(false);
|
||||
|
||||
// --- Post-execution: emit audit event ---
|
||||
// Fire-and-forget: emit the audit event asynchronously without blocking the response
|
||||
_ = EmitAuditEventSafeAsync(context.HttpContext, auditAttr, result);
|
||||
_ = EmitAuditEventSafeAsync(httpContext, auditAttr, result, capturedBody, beforeState, options);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<JsonNode?> CaptureRequestBodyAsync(
|
||||
HttpContext httpContext,
|
||||
AuditActionAttribute auditAttr,
|
||||
AuditEmissionOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
|
||||
// Enable buffering so the body can be read multiple times
|
||||
// (once here, once by the actual endpoint handler)
|
||||
request.EnableBuffering();
|
||||
|
||||
var contentType = request.ContentType;
|
||||
if (string.IsNullOrWhiteSpace(contentType) ||
|
||||
!contentType.Contains("json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (request.ContentLength is > 0 and var length && length > options.MaxBodySizeBytes)
|
||||
{
|
||||
var truncated = new JsonObject
|
||||
{
|
||||
["_truncated"] = true,
|
||||
["_originalSizeBytes"] = length
|
||||
};
|
||||
return truncated;
|
||||
}
|
||||
|
||||
// Read the body
|
||||
request.Body.Position = 0;
|
||||
var maxSize = options.MaxBodySizeBytes;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(maxSize);
|
||||
try
|
||||
{
|
||||
var bytesRead = await request.Body.ReadAsync(buffer.AsMemory(0, maxSize)).ConfigureAwait(false);
|
||||
request.Body.Position = 0; // Reset for the endpoint handler
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var configuredPatterns = options.RedactedFieldPatterns is { Count: > 0 }
|
||||
? (IReadOnlyList<string>)options.RedactedFieldPatterns
|
||||
: null;
|
||||
|
||||
return AuditPiiRedactor.Redact(
|
||||
buffer.AsSpan(0, bytesRead),
|
||||
additionalPatterns: auditAttr.SensitiveFields,
|
||||
configuredPatterns: configuredPatterns);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to capture request body for audit event");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object?>?> CaptureBeforeStateAsync(
|
||||
HttpContext httpContext,
|
||||
AuditActionAttribute auditAttr,
|
||||
string resourceType,
|
||||
string resourceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var providers = httpContext.RequestServices.GetService<IEnumerable<IAuditBeforeStateProvider>>();
|
||||
var provider = providers?.FirstOrDefault(p =>
|
||||
string.Equals(p.Module, auditAttr.Module, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
return await provider.GetBeforeStateAsync(resourceType, resourceId, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to capture before-state for audit event ({Module}/{ResourceType}/{ResourceId})",
|
||||
auditAttr.Module, resourceType, resourceId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EmitAuditEventSafeAsync(
|
||||
HttpContext httpContext,
|
||||
AuditActionAttribute auditAttr,
|
||||
object? result)
|
||||
object? result,
|
||||
JsonNode? capturedBody,
|
||||
Dictionary<string, object?>? beforeState,
|
||||
AuditEmissionOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
var auditEvent = BuildAuditEvent(httpContext, auditAttr, result);
|
||||
var auditEvent = await BuildAuditEventAsync(
|
||||
httpContext, auditAttr, result, capturedBody, beforeState, options, _logger).ConfigureAwait(false);
|
||||
await _emitter.EmitAsync(auditEvent, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -76,10 +214,14 @@ public sealed class AuditActionFilter : IEndpointFilter
|
||||
}
|
||||
}
|
||||
|
||||
internal static AuditEventPayload BuildAuditEvent(
|
||||
internal static async ValueTask<AuditEventPayload> BuildAuditEventAsync(
|
||||
HttpContext httpContext,
|
||||
AuditActionAttribute auditAttr,
|
||||
object? result)
|
||||
object? result,
|
||||
JsonNode? capturedBody,
|
||||
Dictionary<string, object?>? beforeState,
|
||||
AuditEmissionOptions options,
|
||||
ILogger logger)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var user = httpContext.User;
|
||||
@@ -111,6 +253,48 @@ public sealed class AuditActionFilter : IEndpointFilter
|
||||
? corrValues.FirstOrDefault()
|
||||
: httpContext.TraceIdentifier;
|
||||
|
||||
// Build the resource payload, optionally enriched
|
||||
var resourcePayload = new AuditResourcePayload
|
||||
{
|
||||
Type = resourceType,
|
||||
Id = resourceId ?? "unknown"
|
||||
};
|
||||
resourcePayload = await TryEnrichResourceAsync(httpContext, auditAttr, resourcePayload, logger).ConfigureAwait(false);
|
||||
|
||||
// Build details dictionary
|
||||
var details = new Dictionary<string, object?>
|
||||
{
|
||||
["httpMethod"] = request.Method,
|
||||
["requestPath"] = request.Path.Value,
|
||||
["statusCode"] = response.StatusCode
|
||||
};
|
||||
|
||||
// Add request body (redacted)
|
||||
if (capturedBody is not null)
|
||||
{
|
||||
details["requestBody"] = capturedBody;
|
||||
}
|
||||
|
||||
// Add before-state
|
||||
if (beforeState is not null)
|
||||
{
|
||||
details["beforeState"] = beforeState;
|
||||
}
|
||||
|
||||
// Extract response resource ID for create operations
|
||||
var responseResourceId = ExtractResponseResourceId(result);
|
||||
if (responseResourceId is not null)
|
||||
{
|
||||
details["responseResourceId"] = responseResourceId;
|
||||
|
||||
// If we didn't have a resource ID from the route (common for POST/create),
|
||||
// update the resource payload with the newly created ID
|
||||
if (resourcePayload.Id == "unknown")
|
||||
{
|
||||
resourcePayload = resourcePayload with { Id = responseResourceId };
|
||||
}
|
||||
}
|
||||
|
||||
return new AuditEventPayload
|
||||
{
|
||||
Id = $"audit-{Guid.NewGuid():N}",
|
||||
@@ -127,24 +311,117 @@ public sealed class AuditActionFilter : IEndpointFilter
|
||||
IpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = request.Headers.UserAgent.FirstOrDefault()
|
||||
},
|
||||
Resource = new AuditResourcePayload
|
||||
{
|
||||
Type = resourceType,
|
||||
Id = resourceId ?? "unknown"
|
||||
},
|
||||
Resource = resourcePayload,
|
||||
Description = description,
|
||||
Details = new Dictionary<string, object?>
|
||||
{
|
||||
["httpMethod"] = request.Method,
|
||||
["requestPath"] = request.Path.Value,
|
||||
["statusCode"] = response.StatusCode
|
||||
},
|
||||
Details = details,
|
||||
CorrelationId = correlationId,
|
||||
TenantId = tenantId,
|
||||
Tags = [auditAttr.Module.ToLowerInvariant(), auditAttr.Action.ToLowerInvariant()]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backward-compatible static builder for tests that do not require body/enrichment.
|
||||
/// </summary>
|
||||
internal static AuditEventPayload BuildAuditEvent(
|
||||
HttpContext httpContext,
|
||||
AuditActionAttribute auditAttr,
|
||||
object? result)
|
||||
{
|
||||
return BuildAuditEventAsync(
|
||||
httpContext,
|
||||
auditAttr,
|
||||
result,
|
||||
capturedBody: null,
|
||||
beforeState: null,
|
||||
new AuditEmissionOptions(),
|
||||
Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance).AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static async ValueTask<AuditResourcePayload> TryEnrichResourceAsync(
|
||||
HttpContext httpContext,
|
||||
AuditActionAttribute auditAttr,
|
||||
AuditResourcePayload resource,
|
||||
ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (resource.Id == "unknown")
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
|
||||
var enrichers = httpContext.RequestServices.GetService<IEnumerable<IAuditResourceEnricher>>();
|
||||
var enricher = enrichers?.FirstOrDefault(e =>
|
||||
string.Equals(e.Module, auditAttr.Module, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (enricher is null)
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
return await enricher.EnrichAsync(resource.Type, resource.Id, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex, "Audit resource enrichment failed for {Module}/{Type}/{Id}",
|
||||
auditAttr.Module, resource.Type, resource.Id);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to extract an "id" field from the endpoint result, useful for create operations
|
||||
/// where the new resource ID is returned in the response body.
|
||||
/// </summary>
|
||||
internal static string? ExtractResponseResourceId(object? result)
|
||||
{
|
||||
if (result is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Handle IResult wrappers (Results.Ok, Results.Created, etc.)
|
||||
// by serializing to JSON and checking for an "id" property.
|
||||
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for common ID field names
|
||||
string[] idFields = ["id", "Id", "ID", "resourceId", "ResourceId"];
|
||||
foreach (var field in idFields)
|
||||
{
|
||||
if (root.TryGetProperty(field, out var idProp))
|
||||
{
|
||||
return idProp.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => idProp.GetString(),
|
||||
JsonValueKind.Number => idProp.GetRawText(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort; never fail for response inspection
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ResolveClaimValue(ClaimsPrincipal user, params string[] claimTypes)
|
||||
{
|
||||
foreach (var claimType in claimTypes)
|
||||
@@ -159,7 +436,7 @@ public sealed class AuditActionFilter : IEndpointFilter
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ResolveResourceId(HttpContext httpContext)
|
||||
internal static string? ResolveResourceId(HttpContext httpContext)
|
||||
{
|
||||
var routeValues = httpContext.Request.RouteValues;
|
||||
|
||||
|
||||
356
src/__Libraries/StellaOps.Audit.Emission/AuditActions.cs
Normal file
356
src/__Libraries/StellaOps.Audit.Emission/AuditActions.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known action name constants for audit event annotations, organized by module.
|
||||
/// Use these instead of magic strings when calling
|
||||
/// <see cref="AuditedRouteGroupExtensions.Audited"/> or constructing
|
||||
/// <see cref="AuditActionAttribute"/> instances.
|
||||
/// </summary>
|
||||
public static class AuditActions
|
||||
{
|
||||
/// <summary>Actions for the Authority (OAuth/OIDC) module.</summary>
|
||||
public static class Authority
|
||||
{
|
||||
public const string Bootstrap = "bootstrap";
|
||||
public const string Seed = "seed";
|
||||
public const string CreateUser = "create_user";
|
||||
public const string UpdateUser = "update_user";
|
||||
public const string LockUser = "lock_user";
|
||||
public const string UnlockUser = "unlock_user";
|
||||
public const string CreateTenant = "create_tenant";
|
||||
public const string UpdateTenant = "update_tenant";
|
||||
public const string SuspendTenant = "suspend_tenant";
|
||||
public const string ResumeTenant = "resume_tenant";
|
||||
public const string CreateRole = "create_role";
|
||||
public const string UpdateRole = "update_role";
|
||||
public const string CreateClient = "create_client";
|
||||
public const string UpdateClient = "update_client";
|
||||
public const string RotateClientSecret = "rotate_client_secret";
|
||||
public const string RevokeToken = "revoke_token";
|
||||
public const string RotateSigningKey = "rotate_signing_key";
|
||||
public const string RotateAckTokenKey = "rotate_ack_token_key";
|
||||
public const string ReloadPlugins = "reload_plugins";
|
||||
public const string CreateVulnPermalink = "create_vuln_permalink";
|
||||
public const string IssueAckToken = "issue_ack_token";
|
||||
public const string IssueVulnCsrfToken = "issue_vuln_csrf_token";
|
||||
public const string IssueAttachmentToken = "issue_attachment_token";
|
||||
public const string LogRemoteInference = "log_remote_inference";
|
||||
public const string CreateVulnTicket = "create_vuln_ticket";
|
||||
public const string UpdateBranding = "update_branding";
|
||||
public const string RecordAirgapAudit = "record_airgap_audit";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Policy module.</summary>
|
||||
public static class Policy
|
||||
{
|
||||
public const string EnableSealed = "enable_sealed";
|
||||
public const string DisableSealed = "disable_sealed";
|
||||
public const string CreateRiskProfile = "create_risk_profile";
|
||||
public const string ActivateRiskProfile = "activate_risk_profile";
|
||||
public const string DeprecateRiskProfile = "deprecate_risk_profile";
|
||||
public const string ArchiveRiskProfile = "archive_risk_profile";
|
||||
public const string UpdateRiskProfile = "update_risk_profile";
|
||||
public const string DeleteRiskProfile = "delete_risk_profile";
|
||||
public const string CreateSnapshot = "create_snapshot";
|
||||
public const string CreatePack = "create_pack";
|
||||
public const string CreateRevision = "create_revision";
|
||||
public const string ActivateRevision = "activate_revision";
|
||||
public const string CreateOverride = "create_override";
|
||||
public const string DeleteOverride = "delete_override";
|
||||
public const string ApproveOverride = "approve_override";
|
||||
public const string DisableOverride = "disable_override";
|
||||
public const string EnableShadowMode = "enable_shadow_mode";
|
||||
public const string DisableShadowMode = "disable_shadow_mode";
|
||||
public const string CreateSimulation = "create_simulation";
|
||||
public const string UpdateSimulation = "update_simulation";
|
||||
public const string CancelSimulation = "cancel_simulation";
|
||||
public const string BypassGate = "bypass_gate";
|
||||
public const string CreateException = "create_exception";
|
||||
public const string UpdateException = "update_exception";
|
||||
public const string RevokeException = "revoke_exception";
|
||||
public const string ApproveException = "approve_exception";
|
||||
public const string RejectException = "reject_exception";
|
||||
public const string CancelException = "cancel_exception";
|
||||
public const string ActivateException = "activate_exception";
|
||||
public const string ExtendException = "extend_exception";
|
||||
public const string ResolveConflict = "resolve_conflict";
|
||||
public const string DismissConflict = "dismiss_conflict";
|
||||
public const string CreateBatchEvaluation = "create_batch_evaluation";
|
||||
public const string CancelBatchEvaluation = "cancel_batch_evaluation";
|
||||
public const string ToggleSealedMode = "toggle_sealed_mode";
|
||||
public const string EmergencyUnseal = "emergency_unseal";
|
||||
public const string RevokeSealedOverride = "revoke_sealed_override";
|
||||
public const string UpdateTrustWeight = "update_trust_weight";
|
||||
public const string DeleteTrustWeight = "delete_trust_weight";
|
||||
public const string UpdateStalenessConfig = "update_staleness_config";
|
||||
public const string CreateEffectivePolicy = "create_effective_policy";
|
||||
public const string UpdateEffectivePolicy = "update_effective_policy";
|
||||
public const string DeleteEffectivePolicy = "delete_effective_policy";
|
||||
public const string GrantScope = "grant_scope";
|
||||
public const string RevokeScope = "revoke_scope";
|
||||
public const string CreateConflict = "create_conflict";
|
||||
public const string CreateDeltaSnapshot = "create_delta_snapshot";
|
||||
public const string EvaluateGate = "evaluate_gate";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Scanner module.</summary>
|
||||
public static class Scanner
|
||||
{
|
||||
public const string Submit = "submit";
|
||||
public const string SubmitSbom = "submit_sbom";
|
||||
public const string Upload = "upload";
|
||||
public const string Validate = "validate";
|
||||
public const string Verify = "verify";
|
||||
public const string Import = "import";
|
||||
public const string Create = "create";
|
||||
public const string Update = "update";
|
||||
public const string Delete = "delete";
|
||||
public const string Test = "test";
|
||||
public const string Pause = "pause";
|
||||
public const string Resume = "resume";
|
||||
public const string Activate = "activate";
|
||||
public const string TriggerScan = "trigger_scan";
|
||||
public const string Revoke = "revoke";
|
||||
public const string ReceiveWebhook = "receive_webhook";
|
||||
public const string UpdateStatus = "update_status";
|
||||
public const string SubmitVex = "submit_vex";
|
||||
public const string BulkQuery = "bulk_query";
|
||||
public const string GenerateProof = "generate_proof";
|
||||
public const string UploadSarif = "upload_sarif";
|
||||
public const string Analyze = "analyze";
|
||||
public const string Upgrade = "upgrade";
|
||||
public const string BatchAction = "batch_action";
|
||||
public const string Review = "review";
|
||||
public const string Replay = "replay";
|
||||
public const string Compute = "compute";
|
||||
public const string Ingest = "ingest";
|
||||
public const string Reconcile = "reconcile";
|
||||
public const string AttachEntropy = "attach_entropy";
|
||||
public const string AttachReplay = "attach_replay";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Findings module.</summary>
|
||||
public static class Findings
|
||||
{
|
||||
public const string CreateLedgerEvent = "create_ledger_event";
|
||||
public const string CreateAttestationPointer = "create_attestation_pointer";
|
||||
public const string UpdateAttestationPointer = "update_attestation_pointer";
|
||||
public const string CreateSnapshot = "create_snapshot";
|
||||
public const string DeleteSnapshot = "delete_snapshot";
|
||||
public const string CreateAlertDecision = "create_alert_decision";
|
||||
public const string VerifyAlertBundle = "verify_alert_bundle";
|
||||
public const string RegisterVexIssuer = "register_vex_issuer";
|
||||
public const string TransitionFindingState = "transition_finding_state";
|
||||
public const string CreateWebhook = "create_webhook";
|
||||
public const string UpdateWebhook = "update_webhook";
|
||||
public const string DeleteWebhook = "delete_webhook";
|
||||
public const string CreateVexDecision = "create_vex_decision";
|
||||
public const string UpdateVexDecision = "update_vex_decision";
|
||||
public const string CreateFixVerification = "create_fix_verification";
|
||||
public const string UpdateFixVerification = "update_fix_verification";
|
||||
public const string CreateAuditBundle = "create_audit_bundle";
|
||||
public const string CalculateScore = "calculate_score";
|
||||
public const string IngestRuntimeTrace = "ingest_runtime_trace";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Release Orchestrator module.</summary>
|
||||
public static class Release
|
||||
{
|
||||
public const string CreateRelease = "create_release";
|
||||
public const string UpdateRelease = "update_release";
|
||||
public const string DeleteRelease = "delete_release";
|
||||
public const string MarkReady = "mark_ready";
|
||||
public const string PromoteRelease = "promote_release";
|
||||
public const string DeployRelease = "deploy_release";
|
||||
public const string RollbackRelease = "rollback_release";
|
||||
public const string CloneRelease = "clone_release";
|
||||
public const string AddComponent = "add_component";
|
||||
public const string UpdateComponent = "update_component";
|
||||
public const string RemoveComponent = "remove_component";
|
||||
public const string ApprovePromotion = "approve_promotion";
|
||||
public const string RejectPromotion = "reject_promotion";
|
||||
public const string ApprovalDecision = "approval_decision";
|
||||
public const string RollbackRun = "rollback_run";
|
||||
public const string VerifyEvidence = "verify_evidence";
|
||||
public const string CreateDeployment = "create_deployment";
|
||||
public const string PauseDeployment = "pause_deployment";
|
||||
public const string ResumeDeployment = "resume_deployment";
|
||||
public const string CancelDeployment = "cancel_deployment";
|
||||
public const string RollbackDeployment = "rollback_deployment";
|
||||
public const string RetryTarget = "retry_target";
|
||||
public const string ApproveRelease = "approve_release";
|
||||
public const string RejectRelease = "reject_release";
|
||||
public const string BatchApprove = "batch_approve";
|
||||
public const string BatchReject = "batch_reject";
|
||||
public const string CreateScript = "create_script";
|
||||
public const string UpdateScript = "update_script";
|
||||
public const string DeleteScript = "delete_script";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Scheduler (JobEngine) module.</summary>
|
||||
public static class Scheduler
|
||||
{
|
||||
public const string CreateSchedule = "create_schedule";
|
||||
public const string UpdateSchedule = "update_schedule";
|
||||
public const string DeleteSchedule = "delete_schedule";
|
||||
public const string PauseSchedule = "pause_schedule";
|
||||
public const string ResumeSchedule = "resume_schedule";
|
||||
public const string CreateRun = "create_run";
|
||||
public const string CancelRun = "cancel_run";
|
||||
public const string RetryRun = "retry_run";
|
||||
public const string CreatePolicyRun = "create_policy_run";
|
||||
public const string CreateGraphBuild = "create_graph_build";
|
||||
public const string CreateGraphOverlay = "create_graph_overlay";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Notify module.</summary>
|
||||
public static class Notify
|
||||
{
|
||||
public const string CreateTemplate = "create_template";
|
||||
public const string UpdateTemplate = "update_template";
|
||||
public const string DeleteTemplate = "delete_template";
|
||||
public const string CreateRule = "create_rule";
|
||||
public const string UpdateRule = "update_rule";
|
||||
public const string DeleteRule = "delete_rule";
|
||||
public const string CreateQuietHours = "create_quiet_hours";
|
||||
public const string UpdateQuietHours = "update_quiet_hours";
|
||||
public const string DeleteQuietHours = "delete_quiet_hours";
|
||||
public const string CreateOverride = "create_override";
|
||||
public const string RevokeOverride = "revoke_override";
|
||||
public const string UpdateThrottleConfig = "update_throttle_config";
|
||||
public const string DeleteThrottleConfig = "delete_throttle_config";
|
||||
public const string AcknowledgeIncident = "acknowledge_incident";
|
||||
public const string ResolveIncident = "resolve_incident";
|
||||
public const string CreateEscalationPolicy = "create_escalation_policy";
|
||||
public const string UpdateEscalationPolicy = "update_escalation_policy";
|
||||
public const string DeleteEscalationPolicy = "delete_escalation_policy";
|
||||
public const string CreateOncallSchedule = "create_oncall_schedule";
|
||||
public const string UpdateOncallSchedule = "update_oncall_schedule";
|
||||
public const string DeleteOncallSchedule = "delete_oncall_schedule";
|
||||
public const string CreateOncallOverride = "create_oncall_override";
|
||||
public const string DeleteOncallOverride = "delete_oncall_override";
|
||||
public const string StartEscalation = "start_escalation";
|
||||
public const string ManualEscalate = "manual_escalate";
|
||||
public const string StopEscalation = "stop_escalation";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Evidence Locker module.</summary>
|
||||
public static class Evidence
|
||||
{
|
||||
public const string Store = "store";
|
||||
public const string Snapshot = "snapshot";
|
||||
public const string Verify = "verify";
|
||||
public const string Hold = "hold";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Attestor (Signer) module.</summary>
|
||||
public static class Attestor
|
||||
{
|
||||
public const string SignDsse = "sign_dsse";
|
||||
public const string VerifyDsse = "verify_dsse";
|
||||
public const string AddKey = "add_key";
|
||||
public const string RevokeKey = "revoke_key";
|
||||
public const string CreateCeremony = "create_ceremony";
|
||||
public const string ApproveCeremony = "approve_ceremony";
|
||||
public const string ExecuteCeremony = "execute_ceremony";
|
||||
public const string CancelCeremony = "cancel_ceremony";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Integrations module.</summary>
|
||||
public static class Integrations
|
||||
{
|
||||
public const string Create = "create";
|
||||
public const string Update = "update";
|
||||
public const string Delete = "delete";
|
||||
public const string Test = "test";
|
||||
public const string Discover = "discover";
|
||||
public const string RunCodeGuard = "run_code_guard";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Platform module.</summary>
|
||||
public static class Platform
|
||||
{
|
||||
public const string CreateSetupSession = "create_setup_session";
|
||||
public const string ExecuteSetupStep = "execute_setup_step";
|
||||
public const string SkipSetupStep = "skip_setup_step";
|
||||
public const string FinalizeSetup = "finalize_setup";
|
||||
public const string CreateScript = "create_script";
|
||||
public const string UpdateScript = "update_script";
|
||||
public const string DeleteScript = "delete_script";
|
||||
public const string EvaluateScore = "evaluate_score";
|
||||
public const string VerifyScore = "verify_score";
|
||||
public const string CreateEnvironment = "create_environment";
|
||||
public const string UpdateEnvironment = "update_environment";
|
||||
public const string DeleteEnvironment = "delete_environment";
|
||||
public const string UpdateEnvironmentSettings = "update_environment_settings";
|
||||
public const string CreateTarget = "create_target";
|
||||
public const string UpdateTarget = "update_target";
|
||||
public const string DeleteTarget = "delete_target";
|
||||
public const string CreateFreezeWindow = "create_freeze_window";
|
||||
public const string UpdateFreezeWindow = "update_freeze_window";
|
||||
public const string DeleteFreezeWindow = "delete_freeze_window";
|
||||
public const string CreateQuotaAlert = "create_quota_alert";
|
||||
public const string CompleteOnboardingStep = "complete_onboarding_step";
|
||||
public const string SkipOnboarding = "skip_onboarding";
|
||||
public const string UpdateDashboardPreferences = "update_dashboard_preferences";
|
||||
public const string UpdateLanguagePreference = "update_language_preference";
|
||||
public const string UpdateEmailPreference = "update_email_preference";
|
||||
public const string CreateDashboardProfile = "create_dashboard_profile";
|
||||
public const string CreateIdentityProvider = "create_identity_provider";
|
||||
public const string UpdateIdentityProvider = "update_identity_provider";
|
||||
public const string DeleteIdentityProvider = "delete_identity_provider";
|
||||
public const string EnableIdentityProvider = "enable_identity_provider";
|
||||
public const string DisableIdentityProvider = "disable_identity_provider";
|
||||
public const string TestIdentityProvider = "test_identity_provider";
|
||||
public const string ApplyIdentityProvider = "apply_identity_provider";
|
||||
public const string UpdateEnvironmentSetting = "update_environment_setting";
|
||||
public const string DeleteEnvironmentSetting = "delete_environment_setting";
|
||||
public const string UpdateCryptoPreference = "update_crypto_preference";
|
||||
public const string DeleteCryptoPreference = "delete_crypto_preference";
|
||||
public const string CreateTrustKey = "create_trust_key";
|
||||
public const string RotateTrustKey = "rotate_trust_key";
|
||||
public const string RevokeTrustKey = "revoke_trust_key";
|
||||
public const string RegisterTrustIssuer = "register_trust_issuer";
|
||||
public const string BlockTrustIssuer = "block_trust_issuer";
|
||||
public const string UnblockTrustIssuer = "unblock_trust_issuer";
|
||||
public const string RegisterTrustCertificate = "register_trust_certificate";
|
||||
public const string RevokeTrustCertificate = "revoke_trust_certificate";
|
||||
public const string ConfigureTransparencyLog = "configure_transparency_log";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Doctor module.</summary>
|
||||
public static class Doctor
|
||||
{
|
||||
public const string StartRun = "start_run";
|
||||
public const string Diagnose = "diagnose";
|
||||
public const string DeleteReport = "delete_report";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Signals module.</summary>
|
||||
public static class Signals
|
||||
{
|
||||
public const string IngestCallgraph = "ingest_callgraph";
|
||||
public const string IngestRuntimeFact = "ingest_runtime_fact";
|
||||
public const string ComputeReachability = "compute_reachability";
|
||||
public const string IngestUnknowns = "ingest_unknowns";
|
||||
public const string SubmitExecutionEvidence = "submit_execution_evidence";
|
||||
public const string RegisterBeacon = "register_beacon";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Advisory AI module.</summary>
|
||||
public static class AdvisoryAi
|
||||
{
|
||||
public const string CreateRun = "create_run";
|
||||
public const string RecordDecision = "record_decision";
|
||||
public const string RecordOutcome = "record_outcome";
|
||||
}
|
||||
|
||||
/// <summary>Actions for the Risk Engine module.</summary>
|
||||
public static class RiskEngine
|
||||
{
|
||||
public const string CreateScoreJob = "create_score_job";
|
||||
public const string CreateSimulation = "create_simulation";
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,18 @@ public sealed class AuditEmissionOptions
|
||||
/// Default: 3 seconds. Audit emission should be fast and non-blocking.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum request body size (in bytes) to capture in audit events.
|
||||
/// Bodies larger than this limit are truncated with a marker.
|
||||
/// Default: 65536 (64 KB).
|
||||
/// </summary>
|
||||
public int MaxBodySizeBytes { get; set; } = 65_536;
|
||||
|
||||
/// <summary>
|
||||
/// Field name patterns for PII redaction. When set, these replace
|
||||
/// <see cref="AuditPiiRedactor.DefaultRedactedPatterns"/> entirely.
|
||||
/// Each entry is matched case-insensitively against JSON property names.
|
||||
/// </summary>
|
||||
public List<string>? RedactedFieldPatterns { get; set; }
|
||||
}
|
||||
|
||||
@@ -53,6 +53,17 @@ public static class AuditEmissionServiceExtensions
|
||||
{
|
||||
options.TimeoutSeconds = timeout;
|
||||
}
|
||||
|
||||
if (int.TryParse(configuration["AuditEmission:MaxBodySizeBytes"], out var maxBody) && maxBody > 0)
|
||||
{
|
||||
options.MaxBodySizeBytes = maxBody;
|
||||
}
|
||||
|
||||
var redactedPatterns = configuration.GetSection("AuditEmission:RedactedFieldPatterns").Get<List<string>>();
|
||||
if (redactedPatterns is { Count: > 0 })
|
||||
{
|
||||
options.RedactedFieldPatterns = redactedPatterns;
|
||||
}
|
||||
});
|
||||
|
||||
services.AddHttpClient(HttpAuditEventEmitter.HttpClientName, (provider, client) =>
|
||||
|
||||
28
src/__Libraries/StellaOps.Audit.Emission/AuditModules.cs
Normal file
28
src/__Libraries/StellaOps.Audit.Emission/AuditModules.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known module name constants for audit event annotations.
|
||||
/// Use these instead of magic strings when calling
|
||||
/// <see cref="AuditedRouteGroupExtensions.Audited"/> or constructing
|
||||
/// <see cref="AuditActionAttribute"/> instances.
|
||||
/// </summary>
|
||||
public static class AuditModules
|
||||
{
|
||||
public const string Authority = "authority";
|
||||
public const string Policy = "policy";
|
||||
public const string Scanner = "scanner";
|
||||
public const string Findings = "findings";
|
||||
public const string Release = "release";
|
||||
public const string Scheduler = "scheduler";
|
||||
public const string Notify = "notify";
|
||||
public const string Evidence = "evidence";
|
||||
public const string Attestor = "attestor";
|
||||
public const string Integrations = "integrations";
|
||||
public const string Platform = "platform";
|
||||
public const string Doctor = "doctor";
|
||||
public const string Signals = "signals";
|
||||
public const string AdvisoryAi = "advisory-ai";
|
||||
public const string RiskEngine = "riskengine";
|
||||
}
|
||||
164
src/__Libraries/StellaOps.Audit.Emission/AuditPiiRedactor.cs
Normal file
164
src/__Libraries/StellaOps.Audit.Emission/AuditPiiRedactor.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Recursively walks a JSON structure and replaces values whose keys match
|
||||
/// configurable PII/secret patterns with <c>"[REDACTED]"</c>.
|
||||
/// </summary>
|
||||
public static class AuditPiiRedactor
|
||||
{
|
||||
/// <summary>
|
||||
/// Default field name patterns that are always redacted.
|
||||
/// Matching is case-insensitive and uses <c>Contains</c> semantics.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<string> DefaultRedactedPatterns =
|
||||
[
|
||||
"password",
|
||||
"secret",
|
||||
"token",
|
||||
"apikey",
|
||||
"connectionstring",
|
||||
"credential",
|
||||
"privatekey",
|
||||
"signingkey"
|
||||
];
|
||||
|
||||
private const string RedactedValue = "[REDACTED]";
|
||||
|
||||
/// <summary>
|
||||
/// Redacts sensitive fields in a parsed <see cref="JsonNode"/> tree.
|
||||
/// Returns a new tree; the original is not mutated.
|
||||
/// </summary>
|
||||
/// <param name="node">The JSON node to redact. May be null.</param>
|
||||
/// <param name="additionalPatterns">
|
||||
/// Extra field-name patterns to redact beyond <see cref="DefaultRedactedPatterns"/>.
|
||||
/// </param>
|
||||
/// <param name="configuredPatterns">
|
||||
/// Patterns from <see cref="AuditEmissionOptions.RedactedFieldPatterns"/>.
|
||||
/// When non-null these replace the defaults entirely.
|
||||
/// </param>
|
||||
/// <returns>A redacted copy, or null if the input was null.</returns>
|
||||
public static JsonNode? Redact(
|
||||
JsonNode? node,
|
||||
IReadOnlyList<string>? additionalPatterns = null,
|
||||
IReadOnlyList<string>? configuredPatterns = null)
|
||||
{
|
||||
if (node is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var patterns = BuildPatternSet(additionalPatterns, configuredPatterns);
|
||||
return RedactNode(node, patterns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload that accepts raw JSON bytes, redacts, and returns
|
||||
/// the redacted <see cref="JsonNode"/>.
|
||||
/// </summary>
|
||||
public static JsonNode? Redact(
|
||||
ReadOnlySpan<byte> utf8Json,
|
||||
IReadOnlyList<string>? additionalPatterns = null,
|
||||
IReadOnlyList<string>? configuredPatterns = null)
|
||||
{
|
||||
var node = JsonNode.Parse(utf8Json);
|
||||
return Redact(node, additionalPatterns, configuredPatterns);
|
||||
}
|
||||
|
||||
private static HashSet<string> BuildPatternSet(
|
||||
IReadOnlyList<string>? additionalPatterns,
|
||||
IReadOnlyList<string>? configuredPatterns)
|
||||
{
|
||||
var basePatterns = configuredPatterns ?? DefaultRedactedPatterns;
|
||||
var set = new HashSet<string>(basePatterns.Count + (additionalPatterns?.Count ?? 0), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var p in basePatterns)
|
||||
{
|
||||
set.Add(NormalizePattern(p));
|
||||
}
|
||||
|
||||
if (additionalPatterns is not null)
|
||||
{
|
||||
foreach (var p in additionalPatterns)
|
||||
{
|
||||
set.Add(NormalizePattern(p));
|
||||
}
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private static string NormalizePattern(string pattern)
|
||||
{
|
||||
// Strip non-alphanumeric chars so "api_key", "apiKey", "api-key" all match "apikey"
|
||||
return Regex.Replace(pattern, @"[^a-zA-Z0-9]", "", RegexOptions.None, TimeSpan.FromMilliseconds(100))
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string NormalizeFieldName(string fieldName)
|
||||
{
|
||||
return Regex.Replace(fieldName, @"[^a-zA-Z0-9]", "", RegexOptions.None, TimeSpan.FromMilliseconds(100))
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool ShouldRedact(string fieldName, HashSet<string> patterns)
|
||||
{
|
||||
var normalized = NormalizeFieldName(fieldName);
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
if (normalized.Contains(pattern, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static JsonNode? RedactNode(JsonNode node, HashSet<string> patterns)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case JsonObject obj:
|
||||
var result = new JsonObject();
|
||||
foreach (var kvp in obj)
|
||||
{
|
||||
if (ShouldRedact(kvp.Key, patterns))
|
||||
{
|
||||
result[kvp.Key] = JsonValue.Create(RedactedValue);
|
||||
}
|
||||
else if (kvp.Value is null)
|
||||
{
|
||||
result[kvp.Key] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[kvp.Key] = RedactNode(kvp.Value, patterns);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
case JsonArray arr:
|
||||
var resultArr = new JsonArray();
|
||||
foreach (var item in arr)
|
||||
{
|
||||
resultArr.Add(item is null ? null : RedactNode(item, patterns));
|
||||
}
|
||||
|
||||
return resultArr;
|
||||
|
||||
case JsonValue val:
|
||||
// Deep-copy primitive values
|
||||
return JsonNode.Parse(val.ToJsonString());
|
||||
|
||||
default:
|
||||
return JsonNode.Parse(node.ToJsonString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Optional service interface that captures the "before" state of a resource
|
||||
/// prior to endpoint execution, enabling before/after diff in audit events.
|
||||
/// <para>
|
||||
/// Services register implementations in DI. The <see cref="AuditActionFilter"/>
|
||||
/// finds the provider whose <see cref="Module"/> matches the audited endpoint's module
|
||||
/// and calls <see cref="GetBeforeStateAsync"/> before the endpoint executes.
|
||||
/// If no provider is registered or it throws, the before-state is simply omitted.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IAuditBeforeStateProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The module this provider handles (e.g., "scanner", "release").
|
||||
/// Must match the module string on the <see cref="AuditActionAttribute"/>.
|
||||
/// </summary>
|
||||
string Module { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current state of a resource before the endpoint mutates it.
|
||||
/// </summary>
|
||||
/// <param name="resourceType">The type of resource (e.g., "environment", "scan_policy").</param>
|
||||
/// <param name="resourceId">The raw resource identifier (usually a GUID string).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A dictionary of key/value pairs representing the resource's current state,
|
||||
/// or null if the resource does not exist or state capture is not applicable.
|
||||
/// </returns>
|
||||
ValueTask<Dictionary<string, object?>?> GetBeforeStateAsync(string resourceType, string resourceId, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Optional service interface that enriches audit resource payloads by resolving
|
||||
/// a raw resource ID (typically a GUID) into a human-readable name.
|
||||
/// <para>
|
||||
/// Services register implementations in DI. The <see cref="AuditActionFilter"/>
|
||||
/// finds the enricher whose <see cref="Module"/> matches the audited endpoint's module
|
||||
/// and calls <see cref="EnrichAsync"/> in a fire-and-forget fashion.
|
||||
/// If no enricher is registered or it throws, the filter falls back to the raw ID.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IAuditResourceEnricher
|
||||
{
|
||||
/// <summary>
|
||||
/// The module this enricher handles (e.g., "scanner", "release").
|
||||
/// Must match the module string on the <see cref="AuditActionAttribute"/>.
|
||||
/// </summary>
|
||||
string Module { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a resource ID to a human-readable payload containing the display name.
|
||||
/// </summary>
|
||||
/// <param name="resourceType">The type of resource (e.g., "environment", "scan_policy").</param>
|
||||
/// <param name="resourceId">The raw resource identifier (usually a GUID string).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="AuditResourcePayload"/> with the <see cref="AuditResourcePayload.Name"/>
|
||||
/// populated. Returns a payload with the original ID and no name if resolution fails.
|
||||
/// </returns>
|
||||
ValueTask<AuditResourcePayload> EnrichAsync(string resourceType, string resourceId, CancellationToken ct);
|
||||
}
|
||||
Reference in New Issue
Block a user