feat(audit-api): fix 7 gaps — module catalog, Diff ingest, filters, chain verify
- Add release/doctor/signals/advisory-ai/riskengine to module catalog (Gap 1) - Add Diff to UnifiedAuditIngestRequest for before/after state (Gap 2) - Add resourceName, actorIp, actorEmail query parameters (Gap 3, 8) - Add GIN index on details_jsonb for future JSONB queries (Gap 6) - Map chain verification endpoint GET /api/v1/audit/chain/verify (Gap 7) - Expose content_hash + previous_entry_hash in API response (Gap 9) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,7 +117,8 @@ public sealed class PostgresUnifiedAuditEventStore
|
||||
actor_id, actor_name, actor_email, actor_type, actor_ip, actor_user_agent,
|
||||
resource_type, resource_id, resource_name,
|
||||
description, details_jsonb, diff_jsonb,
|
||||
correlation_id, parent_event_id, tags
|
||||
correlation_id, parent_event_id, tags,
|
||||
content_hash, previous_entry_hash
|
||||
FROM timeline.unified_audit_events
|
||||
ORDER BY timestamp DESC, id ASC
|
||||
LIMIT 10000
|
||||
@@ -151,7 +152,8 @@ public sealed class PostgresUnifiedAuditEventStore
|
||||
actor_id, actor_name, actor_email, actor_type, actor_ip, actor_user_agent,
|
||||
resource_type, resource_id, resource_name,
|
||||
description, details_jsonb, diff_jsonb,
|
||||
correlation_id, parent_event_id, tags
|
||||
correlation_id, parent_event_id, tags,
|
||||
content_hash, previous_entry_hash
|
||||
FROM timeline.unified_audit_events
|
||||
WHERE tenant_id = @tenantId
|
||||
ORDER BY timestamp DESC, id ASC
|
||||
@@ -429,7 +431,9 @@ public sealed class PostgresUnifiedAuditEventStore
|
||||
Diff = DeserializeDiff(diffJson),
|
||||
CorrelationId = reader.IsDBNull(18) ? null : reader.GetString(18),
|
||||
ParentEventId = reader.IsDBNull(19) ? null : reader.GetString(19),
|
||||
Tags = tagsArray
|
||||
Tags = tagsArray,
|
||||
ContentHash = reader.IsDBNull(21) ? null : reader.GetString(21),
|
||||
PreviousEntryHash = reader.IsDBNull(22) ? null : reader.GetString(22)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -389,6 +389,24 @@ public sealed class UnifiedAuditAggregationService : IUnifiedAuditAggregationSer
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.ResourceName) &&
|
||||
!ContainsIgnoreCase(auditEvent.Resource.Name, query.ResourceName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.ActorIp) &&
|
||||
!ContainsIgnoreCase(auditEvent.Actor.IpAddress, query.ActorIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.ActorEmail) &&
|
||||
!ContainsIgnoreCase(auditEvent.Actor.Email, query.ActorEmail))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.StartDate.HasValue && auditEvent.Timestamp < query.StartDate.Value)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -18,7 +18,12 @@ public static class UnifiedAuditCatalog
|
||||
"sbom",
|
||||
"evidencelocker",
|
||||
"notify",
|
||||
"scheduler"
|
||||
"scheduler",
|
||||
"release",
|
||||
"doctor",
|
||||
"signals",
|
||||
"advisory-ai",
|
||||
"riskengine"
|
||||
];
|
||||
|
||||
public static readonly IReadOnlyList<string> Actions =
|
||||
@@ -227,6 +232,8 @@ public sealed record UnifiedAuditEvent
|
||||
public string? ParentEventId { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public required IReadOnlyList<string> Tags { get; init; }
|
||||
public string? ContentHash { get; init; }
|
||||
public string? PreviousEntryHash { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedAuditEventsPagedResponse
|
||||
@@ -339,8 +346,11 @@ public sealed record UnifiedAuditQuery
|
||||
public HashSet<string>? Severities { get; init; }
|
||||
public string? ActorId { get; init; }
|
||||
public string? ActorName { get; init; }
|
||||
public string? ActorIp { get; init; }
|
||||
public string? ActorEmail { get; init; }
|
||||
public string? ResourceType { get; init; }
|
||||
public string? ResourceId { get; init; }
|
||||
public string? ResourceName { get; init; }
|
||||
public DateTimeOffset? StartDate { get; init; }
|
||||
public DateTimeOffset? EndDate { get; init; }
|
||||
public string? Search { get; init; }
|
||||
|
||||
@@ -47,6 +47,11 @@ public static class UnifiedAuditEndpoints
|
||||
.WithDescription("Get a unified audit correlation cluster by correlation ID.")
|
||||
.RequireAuthorization(TimelinePolicies.Read);
|
||||
|
||||
group.MapGet("/chain/verify", VerifyChainAsync)
|
||||
.WithName("VerifyUnifiedAuditChain")
|
||||
.WithDescription("Verify the hash chain integrity for a tenant's audit events.")
|
||||
.RequireAuthorization(TimelinePolicies.Read);
|
||||
|
||||
group.MapGet("/anomalies", GetAnomaliesAsync)
|
||||
.WithName("GetUnifiedAuditAnomalies")
|
||||
.WithDescription("Get anomaly alerts from unified audit events.")
|
||||
@@ -119,6 +124,7 @@ public static class UnifiedAuditEndpoints
|
||||
},
|
||||
Description = request.Description ?? $"{action} {module} resource",
|
||||
Details = request.Details ?? new Dictionary<string, object?>(),
|
||||
Diff = request.Diff,
|
||||
CorrelationId = request.CorrelationId,
|
||||
TenantId = request.TenantId,
|
||||
Tags = request.Tags ?? [module, action]
|
||||
@@ -164,6 +170,30 @@ public static class UnifiedAuditEndpoints
|
||||
return Results.Ok(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> VerifyChainAsync(
|
||||
[FromQuery] string tenantId,
|
||||
[FromQuery] long? fromSequence,
|
||||
[FromQuery] long? toSequence,
|
||||
PostgresUnifiedAuditEventStore store,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant_id_required" });
|
||||
}
|
||||
|
||||
var result = await store.VerifyChainAsync(
|
||||
tenantId, fromSequence, toSequence, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
verified = result.IsValid,
|
||||
brokenAt = result.InvalidSequence,
|
||||
brokenEventId = result.InvalidEventId,
|
||||
errorMessage = result.ErrorMessage
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetEventByIdAsync(
|
||||
string eventId,
|
||||
IUnifiedAuditAggregationService service,
|
||||
@@ -296,8 +326,11 @@ public static class UnifiedAuditEndpoints
|
||||
Severities = ParseCsvSet(request.Severities, UnifiedAuditCatalog.Severities),
|
||||
ActorId = NormalizeText(request.ActorId),
|
||||
ActorName = NormalizeText(request.ActorName),
|
||||
ActorIp = NormalizeText(request.ActorIp),
|
||||
ActorEmail = NormalizeText(request.ActorEmail),
|
||||
ResourceType = NormalizeText(request.ResourceType),
|
||||
ResourceId = NormalizeText(request.ResourceId),
|
||||
ResourceName = NormalizeText(request.ResourceName),
|
||||
StartDate = ParseDate(request.StartDate),
|
||||
EndDate = ParseDate(request.EndDate),
|
||||
Search = NormalizeText(request.Search),
|
||||
@@ -375,8 +408,11 @@ public sealed record UnifiedAuditEventsRequest
|
||||
public string? Severities { get; init; }
|
||||
public string? ActorId { get; init; }
|
||||
public string? ActorName { get; init; }
|
||||
public string? ActorIp { get; init; }
|
||||
public string? ActorEmail { get; init; }
|
||||
public string? ResourceType { get; init; }
|
||||
public string? ResourceId { get; init; }
|
||||
public string? ResourceName { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
public string? Search { get; init; }
|
||||
@@ -403,6 +439,7 @@ public sealed record UnifiedAuditIngestRequest
|
||||
public UnifiedAuditIngestResourceRequest? Resource { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public IReadOnlyDictionary<string, object?>? Details { get; init; }
|
||||
public UnifiedAuditDiff? Diff { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public IReadOnlyList<string>? Tags { get; init; }
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Migration: 20260409_004_details_gin_index
|
||||
-- Purpose: Add GIN index on details_jsonb for efficient JSONB queries
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_uae_details_gin
|
||||
ON timeline.unified_audit_events USING GIN (details_jsonb);
|
||||
Reference in New Issue
Block a user