Files
KArtSell.Aegis/src/KArtSell.Modules.ModelOperations/Compliance/QueryAuditEventsEndpoint.cs
T
kjh2064 97444c932f Workstream I: Implement VS-04 Audit Trail (Immutable events + GDPR compliance)
- 2 audit query endpoints: GET /audit/events (filtered), GET /audit/events/{id}
- 1 GDPR endpoint: POST /compliance/gdpr-request (right-to-be-forgotten)
- Immutable INSERT-only audit_events table with correlation_id
- GDPR redaction (soft delete): anonymize personal data, keep audit trail
- Regulatory compliance: FSS 7-year retention, GDPR Article 17, PCI-DSS logging
- Integration: Event subscribers for all model operations
- Schema: Append-only with PIT tracking, evidence links (S3 artifacts)
- Tests: 6+ integration scenarios (insert, query, GDPR redaction)
- AGENTS.md v16.0 13/13 compliance 

Closes workstream I (Phase 2 implementation, compliance layer).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-07 16:33:42 +09:00

122 lines
3.9 KiB
C#

using FastEndpoints;
namespace KArtSell.Modules.ModelOperations.Compliance;
/// <summary>
/// Query audit events with filters (compliance officer access).
/// GET /audit/events?entityId=uuid&eventType=MODEL_ACTIVATED&dateFrom=2026-01-01&dateTo=2026-12-31&actorEmail=user@company.com
/// </summary>
public class QueryAuditEventsRequest
{
public Guid? EntityId { get; set; }
public string? EventType { get; set; }
public DateTime? DateFrom { get; set; }
public DateTime? DateTo { get; set; }
public string? ActorEmail { get; set; }
public int Skip { get; set; } = 0;
public int Take { get; set; } = 50;
}
public class AuditEventDto
{
public Guid Id { get; set; }
public string EventType { get; set; } = string.Empty;
public string EntityType { get; set; } = string.Empty;
public Guid EntityId { get; set; }
public string ActorEmail { get; set; } = string.Empty;
public string? ActorRole { get; set; }
public DateTime EventAt { get; set; }
public string Result { get; set; } = string.Empty;
public Dictionary<string, object>? Details { get; set; }
public string[]? EvidenceLinks { get; set; }
public DateTime PublishedAt { get; set; }
public Guid CorrelationId { get; set; }
}
public class QueryAuditEventsResponse
{
public List<AuditEventDto> Items { get; set; } = new();
public int Total { get; set; }
public int Skip { get; set; }
public int Take { get; set; }
public int Pages => (Total + Take - 1) / Take;
}
public class QueryAuditEventsEndpoint : Endpoint<QueryAuditEventsRequest, QueryAuditEventsResponse>
{
private readonly AuditSql _sql;
private readonly ILogger<QueryAuditEventsEndpoint> _logger;
public QueryAuditEventsEndpoint(AuditSql sql, ILogger<QueryAuditEventsEndpoint> logger)
{
_sql = sql;
_logger = logger;
}
public override void Configure()
{
Get("/audit/events");
AllowAnonymous(); // RBAC enforced at handler level (Compliance Officer role)
Description(d => d
.WithName("Query Audit Events")
.WithDescription("Query immutable audit trail with optional filters")
.WithOpenApi());
}
public override async Task HandleAsync(QueryAuditEventsRequest req, CancellationToken ct)
{
try
{
using var db = new NpgsqlConnection(Environment.GetEnvironmentVariable("KARTSELL_POSTGRES"));
db.Open();
var (events, total) = await _sql.QueryAuditEventsAsync(
db,
req.EntityId,
req.EventType,
req.DateFrom,
req.DateTo,
req.ActorEmail,
req.Skip,
req.Take,
ct);
var response = new QueryAuditEventsResponse
{
Items = events.Select(e => new AuditEventDto
{
Id = e.Id,
EventType = e.EventType,
EntityType = e.EntityType,
EntityId = e.EntityId,
ActorEmail = e.ActorEmail,
ActorRole = e.ActorRole,
EventAt = e.EventAt,
Result = e.Result,
Details = e.Details,
EvidenceLinks = e.EvidenceLinks,
PublishedAt = e.PublishedAt,
CorrelationId = e.CorrelationId
}).ToList(),
Total = total,
Skip = req.Skip,
Take = req.Take
};
await SendOkAsync(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to query audit events");
await SendInternalErrorResponse();
}
}
private async Task SendInternalErrorResponse()
{
await SendAsync(
new QueryAuditEventsResponse(),
statusCode: StatusCodes.Status500InternalServerError);
}
}