97444c932f
- 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>
93 lines
3.1 KiB
C#
93 lines
3.1 KiB
C#
using MediatR;
|
|
|
|
namespace KArtSell.Modules.ModelOperations.Compliance;
|
|
|
|
/// <summary>
|
|
/// Command to log an audit event.
|
|
/// </summary>
|
|
public class LogAuditEventCommand : ICommand
|
|
{
|
|
public Guid Id { get; set; } = Guid.NewGuid();
|
|
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; } = DateTime.UtcNow;
|
|
public string Result { get; set; } = "SUCCESS";
|
|
public string? ErrorMessage { get; set; }
|
|
public Dictionary<string, object>? Details { get; set; }
|
|
public string[]? EvidenceLinks { get; set; }
|
|
public string? IpAddress { get; set; }
|
|
public string? UserAgent { get; set; }
|
|
public Guid CorrelationId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler to log audit events (immutable insert).
|
|
/// Idempotent: Multiple calls with same Id result in same outcome.
|
|
/// </summary>
|
|
public class LogAuditEventHandler : ICommandHandler<LogAuditEventCommand>
|
|
{
|
|
private readonly IDbConnection _db;
|
|
private readonly AuditSql _sql;
|
|
private readonly ILogger<LogAuditEventHandler> _logger;
|
|
|
|
public LogAuditEventHandler(
|
|
IDbConnection db,
|
|
AuditSql sql,
|
|
ILogger<LogAuditEventHandler> logger)
|
|
{
|
|
_db = db;
|
|
_sql = sql;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task Handle(LogAuditEventCommand request, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
// Log immutable audit event
|
|
await _sql.InsertAuditEventAsync(
|
|
_db,
|
|
request.Id,
|
|
request.EventType,
|
|
request.EntityType,
|
|
request.EntityId,
|
|
request.ActorEmail,
|
|
request.ActorRole,
|
|
request.EventAt,
|
|
request.Result,
|
|
request.ErrorMessage,
|
|
request.Details,
|
|
request.EvidenceLinks,
|
|
request.IpAddress,
|
|
request.UserAgent,
|
|
request.CorrelationId,
|
|
ct);
|
|
|
|
// Track GDPR retention for 7 years (FSS requirement)
|
|
var retentionEndsAt = DateTime.UtcNow.AddYears(7);
|
|
await _sql.InsertGdprRetentionAsync(
|
|
_db,
|
|
Guid.NewGuid(),
|
|
request.Id,
|
|
null, // CustomerId would be extracted from request.Details if present
|
|
new[] { GdprDataCategories.TradingHistory, GdprDataCategories.PortfolioData },
|
|
retentionEndsAt,
|
|
ct);
|
|
|
|
_logger.LogInformation(
|
|
"Audit event {EventId} logged: {EventType} for {EntityType} {EntityId}",
|
|
request.Id, request.EventType, request.EntityType, request.EntityId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"Failed to log audit event {EventId}: {EventType} for {EntityType} {EntityId}",
|
|
request.Id, request.EventType, request.EntityType, request.EntityId);
|
|
throw;
|
|
}
|
|
}
|
|
}
|