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:
master
2026-04-09 12:09:27 +03:00
parent 2a69ad112c
commit 7f40f8d678
5 changed files with 78 additions and 4 deletions

View File

@@ -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)
};
}

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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);