From 7f40f8d67855219e79e3eb7f4a3dc2add82f37af Mon Sep 17 00:00:00 2001 From: master <> Date: Thu, 9 Apr 2026 12:09:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(audit-api):=20fix=207=20gaps=20=E2=80=94?= =?UTF-8?q?=20module=20catalog,=20Diff=20ingest,=20filters,=20chain=20veri?= =?UTF-8?q?fy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../Audit/PostgresUnifiedAuditEventStore.cs | 10 +++-- .../Audit/UnifiedAuditAggregationService.cs | 18 +++++++++ .../Audit/UnifiedAuditContracts.cs | 12 +++++- .../Endpoints/UnifiedAuditEndpoints.cs | 37 +++++++++++++++++++ .../20260409_004_details_gin_index.sql | 5 +++ 5 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 src/Timeline/__Libraries/StellaOps.Timeline.Core/Migrations/20260409_004_details_gin_index.sql diff --git a/src/Timeline/StellaOps.Timeline.WebService/Audit/PostgresUnifiedAuditEventStore.cs b/src/Timeline/StellaOps.Timeline.WebService/Audit/PostgresUnifiedAuditEventStore.cs index d2091d735..a9969bc57 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Audit/PostgresUnifiedAuditEventStore.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Audit/PostgresUnifiedAuditEventStore.cs @@ -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) }; } diff --git a/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditAggregationService.cs b/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditAggregationService.cs index c36d8c358..1f15ff069 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditAggregationService.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditAggregationService.cs @@ -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; diff --git a/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditContracts.cs b/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditContracts.cs index 623bb5184..cb9c61a4f 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditContracts.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Audit/UnifiedAuditContracts.cs @@ -18,7 +18,12 @@ public static class UnifiedAuditCatalog "sbom", "evidencelocker", "notify", - "scheduler" + "scheduler", + "release", + "doctor", + "signals", + "advisory-ai", + "riskengine" ]; public static readonly IReadOnlyList Actions = @@ -227,6 +232,8 @@ public sealed record UnifiedAuditEvent public string? ParentEventId { get; init; } public string? TenantId { get; init; } public required IReadOnlyList 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? 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; } diff --git a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/UnifiedAuditEndpoints.cs b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/UnifiedAuditEndpoints.cs index 77c1ca283..f719fad53 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/UnifiedAuditEndpoints.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/UnifiedAuditEndpoints.cs @@ -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(), + 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 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 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? Details { get; init; } + public UnifiedAuditDiff? Diff { get; init; } public string? CorrelationId { get; init; } public string? TenantId { get; init; } public IReadOnlyList? Tags { get; init; } diff --git a/src/Timeline/__Libraries/StellaOps.Timeline.Core/Migrations/20260409_004_details_gin_index.sql b/src/Timeline/__Libraries/StellaOps.Timeline.Core/Migrations/20260409_004_details_gin_index.sql new file mode 100644 index 000000000..e0c9ca65c --- /dev/null +++ b/src/Timeline/__Libraries/StellaOps.Timeline.Core/Migrations/20260409_004_details_gin_index.sql @@ -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);