Audit & Compliance

Complete audit pipeline, entity tracking, SignalR streaming, CSV/Excel/PDF export, and compliance features.

SCRIPE provides a comprehensive audit system that tracks every data modification, API request, and security event. Audit logs are automatically generated by EF Core interceptors, streamed in real-time via SignalR, and exportable in CSV, Excel, and PDF formats.

Audit Architecture

The audit system consists of HTTP request logging and database entity mutation interception. Request metadata is logged asynchronously at the host level via the RequestLoggingMiddleware, while database-level mutations are captured by the AuditableEntityInterceptor before SaveChanges.

Audit Log Pipeline
HTTP Request
RequestLoggingMiddleware — Synchronously captures context, awaits next, Task.Run background write
Pipeline
AstraFlow Mediator Behaviors — LoggingBehavior & StreamLoggingBehavior console tracing
Command / Query Handler — Processes request, triggers unit of work save changes
SaveChanges
AuditableEntityInterceptor — ChangeTracker inspection for CRUD & Soft Delete before saving
LogEntityChange
AuditService — Orchestrates database writes & hub broadcasting
persist
IdentityDbContext (AuditLogs Table) — Multi-provider indexed persistent store
SignalR AuditHub — Broadcasts events to tenant-scoped groups

Hierarchical Tenant Scoping & Security Isolation

Hierarchical Scope Check Logs Traversal
Fetch Log Request — Admin requests audit log lists or detail by ID
DataScopeService Resolution — Resolves effective scope: SuperAdmin, Drill-down, Hierarchy, or OwnTenant
If Hierarchy/Drilldown
TenantHierarchyService — Looks up materialized HierarchyPath (/parent/child/) & caches result
Cache miss
Descendants Lookup (SQL LIKE) — Query: HierarchyPath.StartsWith(parentPath) on indexed path
Security Verification Gate — Checks if requested AuditLog.TenantId matches accessible tenant list
Approved
AuditByTenantScopeSpec — Applies 'WHERE TenantId IN (...)' filter to EF Core query
Secure Data Returned — Returns isolated audit log data

Data isolation is enforced dynamically at query execution. The DataScopeService resolves the administrator's effective scope based on a strict priority chain: ContextTenant (drill-down via AES-encrypted context headers), Permission Overrides, SystemProtectedAdmin, Hierarchy (including children), or OwnTenant. Descendants are traversed in constant time via materialized paths, where starts-with matches translate to index-backed SQL LIKE queries. The repository applies AuditByTenantScopeSpec to ensure 'WHERE TenantId IN (...)' isolation, while direct Guid-based requests are verified by GetAuditLogDetailQueryHandler to prevent horizontal privilege escalation.

AuditLog Entity Structure

AuditLog Entity — Database SchemaC#
/// <summary>
/// Represents a single audit log entry in the database.
/// Captures WHO did WHAT to WHICH entity, WHEN, and from WHERE.
/// </summary>
public class AuditLog : AuditableEntity
{
    public string EventType { get; set; } = string.Empty;
    public string? HttpMethod { get; set; }
    public string? Endpoint { get; set; }
    public string? EntityType { get; set; }
    public string? EntityId { get; set; }
    public string? OldValues { get; set; }
    public string? NewValues { get; set; }
    public string? ChangedProperties { get; set; }
    public Guid? UserId { get; set; }
    public Guid? TenantId { get; set; }
    public string? Username { get; set; }
    public bool IsAdmin { get; set; }
    public string? IpAddress { get; set; }
    public string? UserAgent { get; set; }
    public string? CorrelationId { get; set; }
    public int? StatusCode { get; set; }
    public long? DurationMs { get; set; }
    public bool IsSuccess { get; set; } = true;
    public string? ErrorMessage { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public string? Metadata { get; set; }
    public string? ModuleTag { get; set; }
}
FieldTypeIndexedDescription & Purpose
IdGuidPrimary KeyUnique identifier for the audit record
EventTypestring✅ Single / ✅ CompositeEvent category (Create, Update, Delete, Login, etc.)
TimestampDateTime✅ Single / ✅ CompositeExecution timestamp in UTC. Sorts & filters logs
UserIdGuid?✅ SingleUser ID of the actor executing the transaction
CorrelationIdstring?✅ SingleTrace ID correlating HTTP requests to database transactions
TenantIdGuid?✅ Single / ✅ CompositeTenant context enforcing boundary isolation
Endpointstring?✅ CompositeRequest API endpoint path, indexed with Timestamp
OldValuesstring? (JSON)—JSON serialization of properties before mutation
NewValuesstring? (JSON)—JSON serialization of properties after mutation

Entity Change Interceptor

The AuditableEntityInterceptor hooks into EF Core's SaveChangesAsync pipeline. For every Added, Modified, or Deleted entity (including soft-deletes), it captures the old and new values as JSON, the user who made the change, and the timestamp. It skips AuditLog entities to prevent infinite recursion.

AuditableEntityInterceptor — Change TrackingC#
/// <summary>
/// EF Core interceptor that captures entity changes before SaveChanges.
/// Handles standard CRUD mutations, Soft Deletes, and prevents infinite recursion.
/// </summary>
public class AuditableEntityInterceptor : SaveChangesInterceptor
{
    private readonly IAuditService _auditService;

    public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData, InterceptionResult<int> result, CancellationToken ct)
    {
        var context = eventData.Context;
        if (context == null) return await base.SavingChangesAsync(eventData, result, ct);

        // 1. Intercept Soft Delete BEFORE state is modified by BaseDbContext
        var softDeleteEntries = context.ChangeTracker.Entries<ISoftDeletable>()
            .Where(e => e.State == EntityState.Deleted)
            .ToList();

        foreach (var entry in softDeleteEntries)
        {
            await _auditService.LogEntityChangeAsync(
                AuditEventTypes.Delete,
                entry.Entity.GetType().Name,
                GetEntityId(entry.Entity),
                "Soft Deleted",
                null,
                "IsDeleted",
                ct
            );
        }

        // 2. Intercept Standard CRUD mutations
        foreach (var entry in context.ChangeTracker.Entries())
        {
            if (entry.Entity is AuditLog) // Prevent infinite recursion StackOverflow
                continue;

            if (entry.State == EntityState.Unchanged || entry.State == EntityState.Detached)
                continue;

            string eventType = entry.State switch
            {
                EntityState.Added => AuditEventTypes.Create,
                EntityState.Modified => AuditEventTypes.Update,
                EntityState.Deleted => AuditEventTypes.Delete,
                _ => "Unknown"
            };

            var oldValues = new Dictionary<string, object?>();
            var newValues = new Dictionary<string, object?>();
            var changedProps = new List<string>();

            foreach (var prop in entry.Properties)
            {
                if (prop.Metadata.IsPrimaryKey()) continue;

                if (entry.State == EntityState.Added)
                {
                    newValues[prop.Metadata.Name] = prop.CurrentValue;
                }
                else if (entry.State == EntityState.Modified && prop.IsModified)
                {
                    var original = entry.OriginalValues[prop.Metadata.Name];
                    var current = prop.CurrentValue;
                    if (!Equals(original, current))
                    {
                        oldValues[prop.Metadata.Name] = original;
                        newValues[prop.Metadata.Name] = current;
                        changedProps.Add(prop.Metadata.Name);
                    }
                }
                else if (entry.State == EntityState.Deleted)
                {
                    oldValues[prop.Metadata.Name] = entry.OriginalValues[prop.Metadata.Name];
                }
            }

            if (entry.State == EntityState.Modified && !changedProps.Any())
                continue; // Skip if no properties changed

            await _auditService.LogEntityChangeAsync(
                eventType,
                entry.Entity.GetType().Name,
                GetEntityId(entry.Entity),
                entry.State.ToString(),
                Serialize(oldValues),
                Serialize(newValues),
                ct
            );
        }

        return await base.SavingChangesAsync(eventData, result, ct);
    }
}

Real-Time SignalR Streaming

Audit logs are broadcast in real-time via the AuditHub SignalR hub. Connected admin clients receive instant notifications when any data changes occur, enabling live monitoring dashboards.

AuditHub.cs — SignalR HubC#
/// <summary>
/// SignalR hub for real-time audit log streaming.
/// Clients join tenant-specific groups for scoped updates.
/// </summary>
[Authorize]
public class AuditHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        var tenantId = Context.User?.FindFirst("tenant_id")?.Value;
        if (tenantId != null)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, tenantId);
        }
        await base.OnConnectedAsync();
    }
}

// In IAuditService — broadcast after persisting
public async Task LogAsync(AuditEntry entry)
{
    await _dbContext.AuditLogs.AddAsync(entry.ToAuditLog());
    await _dbContext.SaveChangesAsync();

    // Broadcast to tenant group in real-time
    await _hubContext.Clients
        .Group(entry.TenantId.ToString())
        .SendAsync("AuditLogCreated", entry.ToDto());
}

Export Capabilities

The AuditExportService provides multi-format exports. CSV export uses CsvHelper with quotes around all fields to prevent CSV injection (RFC 4180) and a UTF-8 BOM prefix for Excel. Excel export generates a ClosedXML workbook with three detailed sheets: Executive Summary (dashboard stats/KPIs), Audit Data (with auto-filters, frozen headers, and green/red conditional formatting), and Security Analysis. PDF export uses QuestPDF layout engine, which is marked as Obsolete for large datasets due to memory footprint. To protect resources, all exports are capped to a maximum of 10,000 rows and loaded in-memory before transmission.

Export FormatServiceFeatures
CSVCsvExportServiceLightweight, spreadsheet-compatible, RFC 4180 quote enforcement, UTF-8 BOM for Excel
Excel (.xlsx)ExcelExportServiceClosedXML workbook with Executive Summary (KPI metrics dashboard), Audit Data (auto-filters, frozen headers, green/red conditional formatting), and Security Analysis sheets
PDFPdfExportServiceQuestPDF community license, Cover page, executive summary charts, Obsolete for large datasets due to RAM overhead
ExportService — Common InterfaceC#
public interface IExportService
{
    Task<byte[]> ExportAsync<T>(
        IEnumerable<T> data,
        ExportOptions options,
        CancellationToken ct = default);
}

public class ExportOptions
{
    public string Title { get; set; } = "Export";
    public string[] Columns { get; set; } = Array.Empty<string>();
    public string DateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
    public bool IncludeHeaders { get; set; } = true;
    public string? WatermarkText { get; set; }     // PDF only
    public string? BrandingLogoPath { get; set; }   // PDF only
}

Query & Export API

The audit API provides search, filter, and export capabilities for audit logs. All endpoints require admin authentication and the audit.view or audit.export permission.

MethodEndpointDescriptionAuth
GET/api/v1/auditSearch and filter audit logs with pagination🔒 Required
GET/api/v1/audit/export?format=csvExport audit logs as CSV file🔒 Required
GET/api/v1/audit/export?format=excelExport audit logs as Excel spreadsheet🔒 Required
GET/api/v1/audit/export?format=pdfExport audit logs as PDF document🔒 Required
Search Audit Logs — Query ParametersJSON
GET /api/v1/audit?
  userId=a1b2c3d4...
  &action=Modified
  &entityName=Admin
  &from=2026-01-01T00:00:00Z
  &to=2026-02-20T23:59:59Z
  &search=password
  &page=1
  &pageSize=25
  &sortBy=timestamp
  &sortDirection=desc

Compliance Features

  • Full Trace

    Captures HTTP headers, request context, and entity mutations to ensure complete trace visibility.

  • Immutable Logs

    Logs are locked and stored in a read-only fashion, preventing deletion or updates once committed.

  • Tenant Scoped

    Logs are automatically isolated by TenantId and descendant tree boundaries, preventing leaks.

  • Retention Policy

    Configured retention periods automatically purge expired audit logs via background workers.

  • Searchable

    Optimized indexes on Timestamp, UserId, EventType, and CorrelationId enable instant lookup.

  • Real-Time

    Stream live security events and mutations directly to tenant-scoped SignalR dashboards.