Skip to content

jesseRules/MillWorks.AuditCore

MillWorks.AuditCore

Tamper-evident audit logging and compliance platform for .NET

Build Status NuGet License: MIT .NET 10 Tests

MillWorks.AuditCore is a comprehensive audit logging framework for .NET applications that enforces data integrity at the storage layer through cryptographic hash chains and HMAC signatures. Built for organizations operating under HIPAA, FERPA, SOC 2, GDPR, and IRB requirements, it provides tamper-evident logging, field-level encryption, and automated compliance validation -- capabilities that are typically spread across multiple commercial products. The library integrates with Entity Framework Core as a SaveChanges interceptor, capturing every entity change with zero modifications to existing application code.

Compatibility

Component Requirement
.NET .NET 10.0+
Entity Framework Core 10.0+
SQL Server 2016+ (or Azure SQL)
Redis (optional) 6.0+ — required only for distributed locking and Redis dead letter queue
Azure Blob Storage (optional) Required only for archival

Versioning policy: This project follows Semantic Versioning 2.0. The public API surface consists of the builder API (AddMillWorksAudit), the IAuditProvider contract, and all types in the MillWorks.AuditCore.Abstractions package.

Packages

Package Purpose
MillWorks.AuditCore Primary install — batteries included, pulls in EF Core + ASP.NET Core wiring
MillWorks.AuditCore.Abstractions Pure .NET — models, DTOs, interfaces. No EF or ASP.NET dependencies. Reference this from shared libraries or non-web hosts.
MillWorks.AuditCore.EntityFramework EF Core data layer without ASP.NET host dependencies
MillWorks.AuditCore.Services Business logic — compliance, encryption, dead letter queue, query/reporting
MillWorks.AuditCore.Providers Entity-specific audit enrichment via IAuditProvider

Most consumers install only MillWorks.AuditCore. The other packages are available for advanced scenarios (e.g., referencing Abstractions from a shared domain library that should not depend on ASP.NET).

Features

Automatic Entity Auditing

EF Core SaveChangesInterceptor automatically captures create, update, and delete operations across all tracked entities. Entities are diffed at the property level, recording old values, new values, and changed property lists. No attribute decoration or manual logging calls required -- opt out with [NoAudit] when needed.

Tamper Detection

Every audit event is linked into a cryptographic hash chain. Each record's SHA-256 hash incorporates the previous record's hash, forming an append-only ledger that detects insertion, deletion, or modification of any record in the sequence. Chain integrity can be verified on demand or on a schedule. Tamper alerts are recorded as security events.

Field-Level Encryption

AES-256-GCM encryption for sensitive audit fields, applied transparently through EF Core value converters. Mark properties with [EncryptedField] or [SensitiveData(AutoEncrypt = true)]. Key management supports Azure Key Vault for cloud deployments or file-based key storage for DMZ and air-gapped environments. Per-field key derivation ensures compromise of one field does not expose others.

Compliance Validation

Built-in validators for seven regulatory standards: GDPR (Articles 17, 25, 30, 32), HIPAA (45 CFR Part 164), FERPA (34 CFR Part 99), SOC 2 (Trust Services Criteria), ISO 27001 (Annex A), PCI-DSS, and STIG. Validators inspect the audit log for required controls and produce structured compliance reports with pass/fail per rule, severity, regulation references, and remediation recommendations.

Enforcement modes control what happens when a non-compliant operation is intercepted:

Mode Behavior
Advisory (default) Log a warning. The operation proceeds normally.
AuditOnly Log a warning and create an AuditSecurityEvent record. The operation proceeds.
Enforce Block the operation by throwing a ComplianceViolationException. The exception includes the standard name, entity type, user ID, and regulation reference (e.g., "34 CFR §99.30"). Callers should catch this exception and return an appropriate error response.

Dead Letter Queue

Audit events that fail to persist (database timeout, transient fault) are captured in a dead letter queue rather than lost. A background processor automatically retries failed events with configurable retry policies.

All three providers ship in v1.0:

Provider Backing Store Best For
InMemory ConcurrentDictionary Development and testing
FileSystem JSON files with semaphore locking Single-instance production without Redis
Redis Sorted sets with 30-day expiry Multi-instance production deployments

Distributed Coordination

Redis-based distributed locking ensures hash chain consistency across multiple application instances writing audit events concurrently. Falls back to in-memory locking for single-instance deployments.

Archival

Completed audit records can be archived to Azure Blob Storage with integrity verification. Archives are checksummed and can be restored on demand. Background archival runs on a configurable schedule with retention policies.

Custom Providers

Per-entity audit enrichment through the IAuditProvider interface. Register providers for specific entity types to control which actions are audited, add domain-specific metadata, and mask sensitive properties before they reach the audit log.

Query and Reporting

Full-text search, date-range filtering, entity trail reconstruction, user activity timelines, event type distribution, and top-user reports. Compliance reports can be generated per standard for any date range. All query services use AsNoTracking() for read performance.

Quick Start

Install the primary package (pulls in all dependencies):

dotnet add package MillWorks.AuditCore

Minimal Setup

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMillWorksAudit(audit =>
{
    audit.Options.ApplicationName = "MyApp";

    audit.UseEntityFramework(ef =>
    {
        ef.ConnectionString = builder.Configuration
            .GetConnectionString("DefaultConnection")!;
        ef.EnsureDatabaseCreated = true;
        ef.Schema = "audit";
    });

    audit.UseSecurity(security =>
    {
        security.EnableTamperDetection = true;
    });
});

var app = builder.Build();
app.UseMillWorksAudit();
app.Run();

Manual Logging

Inject IAuditLogger anywhere in your application:

public class OrderService(IAuditLogger auditLogger)
{
    public async Task PlaceOrderAsync(Order order)
    {
        // ... business logic ...

        await auditLogger.LogAsync("Order.Placed", new
        {
            OrderId = order.Id,
            Total = order.Total,
            ItemCount = order.Items.Count
        });
    }
}

Scoped Operations

For multi-step operations, use audit scopes to accumulate context:

await using var scope = auditLogger.CreateScope("Order.Fulfillment", order);
scope.SetCustomField("WarehouseId", warehouse.Id);

// ... pick, pack, ship ...

scope.SetCustomField("TrackingNumber", trackingNumber);
scope.SetCustomField("ShippedAt", DateTimeOffset.UtcNow);
// Event is persisted with all fields when the scope disposes

Automatic Entity Change Tracking

Any entity saved through a DbContext that has the audit interceptor registered will be captured automatically:

dbContext.Products.Add(new Product { Name = "Widget", Price = 9.99m });
await dbContext.SaveChangesAsync();
// AuditEvent is created with Action = Created, entity name, key values,
// and a full snapshot of the new property values -- no code changes needed.

The IAuditProvider Contract

IAuditProvider is the primary extensibility point for per-entity audit behavior. Implement this interface to control what gets audited and how audit events are enriched for a given entity type.

public interface IAuditProvider
{
    /// The entity type name this provider handles (e.g., "Patient", "FinancialRecord")
    string EntityType { get; }

    /// Create an audit event for the given action and entity state
    Task<AuditEvent> CreateAuditEventAsync(string action, object? entity, object? oldValues = null);

    /// Return false to suppress auditing for a specific action/entity combination
    Task<bool> ShouldAuditAsync(string action, object entity);

    /// Add domain-specific metadata to an audit event after creation
    Task EnrichAuditEventAsync(AuditEvent auditEvent, object? entity);

    /// Compute a property-level diff between old and new entity state
    Dictionary<string, object?> GetChanges(object? oldValues, object? newValues);
}

A BaseAuditProvider abstract class provides default implementations for change tracking, HTTP context integration (IP address, user agent, request ID), and reflection-based property comparison. Most providers only need to override EntityType and optionally ShouldAuditAsync:

public class PatientAuditProvider : BaseAuditProvider
{
    public PatientAuditProvider(IHttpContextAccessor httpContextAccessor)
        : base(httpContextAccessor) { }

    public override string EntityType => "Patient";

    public override Task<bool> ShouldAuditAsync(string action, object entity)
    {
        // Audit all actions on Patient entities
        return Task.FromResult(true);
    }
}

Register providers in the builder:

audit.RegisterProviders(registry =>
{
    registry.AddProvider<PatientAuditProvider>("Patient");
    registry.AddProvider<FinancialRecordAuditProvider>("FinancialRecord");
});

Opting Out with [NoAudit]

The [NoAudit] attribute can be applied at two levels:

  • Entity level ([NoAudit] class TempCache { ... }) — the entire entity type is excluded from automatic audit capture.
  • Property level ([NoAudit] public string InternalNotes { get; set; }) — the property is excluded from change tracking and will not appear in old/new value diffs.

[NoAudit] applies only to the decorated target. It does not cascade to navigation properties or owned entities. If you need to exclude a related entity, apply [NoAudit] to that entity's class directly.

Architecture

Abstractions          (pure .NET -- no EF, no ASP.NET dependencies)
  Models, DTOs, Interfaces, Enums, Constants, Requests, Responses
    |
EntityFramework       (EF Core data layer)
  DbContext, Entities, Interceptor, Repositories, Migrations, Value Converters
    |
Providers             (entity-specific audit enrichment)
  IAuditProvider, BaseAuditProvider, per-entity implementations
    |
Services              (business logic)
  Compliance validators, Tamper detection, Encryption, Dead letter queue,
  Query/Search/Report, Archival, Maintenance, Mapping
    |
AspNetCore            (application entry point)
  MillWorksAuditBuilder, ServiceCollectionExtensions, Middleware, Options

Abstractions is a pure .NET library with no framework dependencies. It can be referenced from console applications, background workers, Azure Functions, or any .NET host without pulling in ASP.NET Core or Entity Framework.

Each layer depends only on the layers below it. The AspNetCore package is the top-level integration point that wires everything together through dependency injection.

Configuration

The full builder API:

builder.Services.AddMillWorksAudit(audit =>
{
    // Application identity
    audit.Options.ApplicationName = "MyApp";
    audit.Options.Environment = "Production";     // Default: "Production"
    audit.Options.EnableDigitalSignatures = true;
    audit.Options.HmacKey = builder.Configuration["Audit:HmacKey"]!;

    // Entity Framework storage (required)
    audit.UseEntityFramework(ef =>
    {
        ef.ConnectionString = "Server=...";
        ef.Schema = "audit";                // SQL Server schema (default: "audit")
        ef.MigrateOnStartup = true;         // Apply EF migrations on startup (default: false)
        ef.EnsureDatabaseCreated = false;    // Use EnsureCreated for dev (default: true)
        ef.MigrationTimeoutSeconds = 120;    // Default: 300
    });

    // Security and tamper detection
    audit.UseSecurity(security =>
    {
        security.EnableTamperDetection = true;   // Default: true
        security.UseRedisLocking = true;         // Default: false
        security.RedisConnectionString = "localhost:6379";
    });

    // Compliance validation
    audit.UseCompliance(compliance =>
    {
        compliance.Standards.Add(ComplianceStandard.HIPAA);
        compliance.Standards.Add(ComplianceStandard.FERPA);
        compliance.Standards.Add(ComplianceStandard.SOC2);
        compliance.EnableAutomaticValidation = true;  // Default: true
        compliance.DataRetentionDays = 2555;          // 7 years for HIPAA (default: 365)
        compliance.EnforcementMode = ComplianceEnforcementMode.Enforce;  // Default: Advisory
    });

    // Archival to Azure Blob Storage
    audit.UseArchival(archival =>
    {
        archival.Provider = ArchivalProvider.AzureBlob;
        archival.ConnectionString = "<azure-storage-connection>";
        archival.ContainerName = "audit-archives";
        archival.EnableBackgroundArchival = true;
        archival.RetentionDays = 365;
        archival.ArchivalIntervalHours = 24;
    });

    // Resilience and dead letter queue
    audit.UseResilience(resilience =>
    {
        resilience.EnableDeadLetterQueue = true;                        // Default: true
        resilience.DeadLetterProvider = DeadLetterProvider.FileSystem;   // Default: InMemory
        resilience.EnableBackgroundProcessor = true;                     // Default: true
        resilience.MaxRetries = 3;                                      // Default: 3
    });

    // Field-level encryption (Azure Key Vault)
    audit.UseFieldEncryption("https://my-vault.vault.azure.net/");

    // Or file-based encryption for air-gapped environments
    // audit.UseFieldEncryptionWithFileStorage("/secure/keys", "<master-key>");

    // Custom per-entity audit providers
    audit.RegisterProviders(registry =>
    {
        registry.AddProvider<PatientAuditProvider>("Patient");
        registry.AddProvider<FinancialRecordAuditProvider>("FinancialRecord");
    });
});

Security note: The HMAC key is a cryptographic secret. Do not hard-code it in source or check it into version control. Use .NET User Secrets for local development, and a secrets manager (Azure Key Vault, AWS Secrets Manager, environment variables) in deployed environments.

Database Initialization Defaults

Option Default Behavior
EnsureDatabaseCreated true Calls EnsureCreated() on startup — creates the schema if the database does not exist. Suitable for development.
MigrateOnStartup false Applies EF Core migrations on startup. Use this in production for schema evolution.

If both are set to false, the application assumes the audit schema already exists. The first audit write will throw a DbUpdateException if the tables are missing. For production deployments, either enable MigrateOnStartup or apply migrations as part of your deployment pipeline (dotnet ef database update).

Compliance Standards

Standard Scope What the Validator Checks
GDPR EU data protection Records of processing (Art. 30), right to erasure support (Art. 17), data protection by design (Art. 25), security of processing (Art. 32), user identification in audit trails
HIPAA US health information Audit controls (SS 164.312(b)), access controls, integrity controls, transmission security, person/entity authentication, emergency access procedures, automatic logoff tracking
FERPA US education records Access logging for education records, consent verification, directory information handling, legitimate educational interest tracking, disclosure logging
SOC 2 Trust Services Criteria Logical access controls, system operations monitoring, change management tracking, risk mitigation evidence, availability and processing integrity
ISO 27001 Information security Annex A control coverage: access control, cryptography, operations security, communications security, incident management, business continuity, compliance evidence
PCI-DSS Payment card data Cardholder data access tracking, authentication monitoring, network access logging, system component change tracking, security event alerting
STIG DoD security baselines Security-relevant event logging, access control enforcement, audit record content requirements, timestamp accuracy, audit storage capacity monitoring

Database Schema

All tables are created under a configurable SQL Server schema (default: audit).

Table Purpose Key Columns
AuditEvents Primary audit event store Id, EventType, EntityName, Action, UserId, JsonData, StartDate, CorrelationId
AuditLogs Entity change log with old/new values Id, EntityName, EntityId, Action, OldValues, NewValues, ChangedProperties, UserId
AuditIntegrity Hash chain records for tamper detection Id, AuditEventId, EventHash, PreviousHash, SequenceNumber, HmacSignature
AuditArchiveRecords Metadata for archived audit batches Id, ArchiveId, BlobPath, EventCount, Checksum, ArchivedAt, RestoredAt
AuditSecurityEvents Security-relevant events and tamper alerts Id, EventType, Severity, Description, SourceIp, DetectedAt

Append-only entities (AuditEvents, AuditIntegrity, AuditSecurityEvents) do not carry update/delete audit columns to avoid unnecessary storage overhead.

Contributing

See CONTRIBUTING.md for development setup, coding standards, and pull request guidelines.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Citation

If you use MillWorks.AuditCore in academic work or grant-funded research, please cite it:

Carlberg, J. (2025). MillWorks.AuditCore: Tamper-evident audit logging and compliance platform for .NET. https://github.com/jesserules/millworks.auditcore

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors