Marketplace

audit-logging

Immutable audit logging patterns for compliance and security. Covers event design, storage strategies, retention policies, and audit trail analysis.

allowed_tools: Read, Glob, Grep, Task, mcp__perplexity__search, mcp__perplexity__reason, mcp__microsoft-learn__microsoft_docs_search, mcp__microsoft-learn__microsoft_docs_fetch

$ Installer

git clone https://github.com/melodic-software/claude-code-plugins /tmp/claude-code-plugins && cp -r /tmp/claude-code-plugins/plugins/saas-patterns/skills/audit-logging ~/.claude/skills/claude-code-plugins

// tip: Run this command in your terminal to install the skill


name: audit-logging description: Immutable audit logging patterns for compliance and security. Covers event design, storage strategies, retention policies, and audit trail analysis. allowed-tools: Read, Glob, Grep, Task, mcp__perplexity__search, mcp__perplexity__reason, mcp__microsoft-learn__microsoft_docs_search, mcp__microsoft-learn__microsoft_docs_fetch

Audit Logging Skill

Patterns for implementing immutable audit logs that meet compliance requirements and enable security analysis.

When to Use This Skill

Use this skill when:

  • Audit Logging tasks - Working on immutable audit logging patterns for compliance and security. covers event design, storage strategies, retention policies, and audit trail analysis
  • Planning or design - Need guidance on Audit Logging approaches
  • Best practices - Want to follow established patterns and standards

Overview

Audit logs provide a tamper-evident record of who did what, when, and from where. They are essential for compliance (SOC 2, HIPAA, GDPR), security investigations, and operational troubleshooting.

Audit Log Architecture

+------------------------------------------------------------------+
|                     Audit Logging Pipeline                        |
+------------------------------------------------------------------+
|                                                                   |
|  +-------------+    +---------------+    +--------------------+   |
|  | Application |    | Audit Service |    | Immutable Storage  |   |
|  | (emit)      |--->| (enrich,      |--->| (append-only,      |   |
|  |             |    |  validate)    |    |  tamper-evident)   |   |
|  +-------------+    +---------------+    +--------------------+   |
|        |                   |                      |               |
|        v                   v                      v               |
|  User actions         Timestamp,            WORM storage,         |
|  System events        correlation,          blockchain hash,      |
|  Data changes         tenant context        retention lock        |
|                                                                   |
+------------------------------------------------------------------+

Audit Event Design

Core Event Schema

public sealed record AuditEvent
{
    // Identity
    public required Guid EventId { get; init; } = Guid.NewGuid();
    public required DateTimeOffset Timestamp { get; init; }
    public required string EventType { get; init; }  // "user.login", "data.export", etc.

    // Actor
    public required Guid? UserId { get; init; }
    public required Guid TenantId { get; init; }
    public string? UserEmail { get; init; }
    public string? UserRole { get; init; }
    public required ActorType ActorType { get; init; }  // User, System, API, Service

    // Context
    public required string Source { get; init; }  // "web", "api", "background-job"
    public string? IpAddress { get; init; }
    public string? UserAgent { get; init; }
    public string? SessionId { get; init; }
    public string? CorrelationId { get; init; }
    public string? RequestId { get; init; }

    // Action
    public required string Action { get; init; }  // "create", "read", "update", "delete"
    public required string ResourceType { get; init; }  // "user", "invoice", "settings"
    public string? ResourceId { get; init; }
    public required bool Success { get; init; }
    public string? FailureReason { get; init; }

    // Change Details (for modifications)
    public Dictionary<string, object?>? OldValues { get; init; }
    public Dictionary<string, object?>? NewValues { get; init; }

    // Metadata
    public Dictionary<string, string>? Tags { get; init; }
}

public enum ActorType
{
    User,
    System,
    ApiClient,
    BackgroundService,
    ExternalIntegration
}

Event Types Taxonomy

Event Type Hierarchy:
+------------------------------------------------------------------+
| Category          | Event Types                                  |
+-------------------+----------------------------------------------+
| Authentication    | user.login, user.logout, user.login_failed,  |
|                   | user.mfa_enabled, user.password_changed      |
+-------------------+----------------------------------------------+
| Authorization     | access.granted, access.denied,               |
|                   | permission.changed, role.assigned            |
+-------------------+----------------------------------------------+
| Data Operations   | data.created, data.updated, data.deleted,    |
|                   | data.exported, data.imported                 |
+-------------------+----------------------------------------------+
| Administration    | settings.changed, user.invited,              |
|                   | integration.enabled, api_key.created         |
+-------------------+----------------------------------------------+
| Billing           | subscription.created, payment.processed,     |
|                   | invoice.generated, plan.changed              |
+-------------------+----------------------------------------------+
| Security          | security.alert, ip.blocked,                  |
|                   | suspicious.activity, breach.detected         |
+-------------------+----------------------------------------------+

Implementation Patterns

Audit Service Interface

public interface IAuditService
{
    // Log an audit event
    Task LogAsync(AuditEvent auditEvent, CancellationToken ct = default);

    // Log with builder pattern
    Task LogAsync(Action<AuditEventBuilder> configure, CancellationToken ct = default);

    // Query audit log (for admin/compliance)
    Task<PagedResult<AuditEvent>> QueryAsync(
        AuditQuery query,
        CancellationToken ct = default);

    // Export for compliance reports
    Task<Stream> ExportAsync(
        AuditExportRequest request,
        CancellationToken ct = default);
}

public sealed class AuditEventBuilder
{
    private readonly AuditEvent _event = new();

    public AuditEventBuilder WithUser(Guid userId, string? email = null)
    {
        _event = _event with { UserId = userId, UserEmail = email };
        return this;
    }

    public AuditEventBuilder WithAction(string eventType, string action, bool success = true)
    {
        _event = _event with { EventType = eventType, Action = action, Success = success };
        return this;
    }

    public AuditEventBuilder WithResource(string resourceType, string? resourceId)
    {
        _event = _event with { ResourceType = resourceType, ResourceId = resourceId };
        return this;
    }

    public AuditEventBuilder WithChanges(object? oldValue, object? newValue)
    {
        // Serialize to dictionaries, redacting sensitive fields
        return this;
    }

    public AuditEvent Build() => _event;
}

Automatic Audit via Interceptors

// EF Core interceptor for automatic data change auditing
public sealed class AuditSaveChangesInterceptor(
    IAuditService auditService,
    ITenantContext tenantContext,
    ICurrentUserService currentUser) : SaveChangesInterceptor
{
    public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken ct = default)
    {
        var context = eventData.Context;
        if (context is null) return result;

        var auditableEntries = context.ChangeTracker
            .Entries()
            .Where(e => e.Entity is IAuditable)
            .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
            .ToList();

        foreach (var entry in auditableEntries)
        {
            var auditEvent = new AuditEvent
            {
                EventId = Guid.NewGuid(),
                Timestamp = DateTimeOffset.UtcNow,
                EventType = $"data.{entry.State.ToString().ToLowerInvariant()}",
                UserId = currentUser.UserId,
                TenantId = tenantContext.TenantId,
                ActorType = ActorType.User,
                Source = "api",
                Action = entry.State.ToString().ToLowerInvariant(),
                ResourceType = entry.Entity.GetType().Name,
                ResourceId = GetPrimaryKey(entry),
                Success = true,
                OldValues = entry.State != EntityState.Added
                    ? GetValues(entry.OriginalValues)
                    : null,
                NewValues = entry.State != EntityState.Deleted
                    ? GetValues(entry.CurrentValues)
                    : null
            };

            await auditService.LogAsync(auditEvent, ct);
        }

        return result;
    }
}

Action Filter for API Auditing

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class AuditAttribute : Attribute, IAsyncActionFilter
{
    public string EventType { get; set; } = "api.request";
    public string? ResourceType { get; set; }
    public bool LogRequestBody { get; set; }
    public bool LogResponseBody { get; set; }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var auditService = context.HttpContext.RequestServices
            .GetRequiredService<IAuditService>();

        var startTime = DateTimeOffset.UtcNow;
        var result = await next();

        var auditEvent = new AuditEvent
        {
            EventType = EventType,
            Timestamp = startTime,
            Action = context.HttpContext.Request.Method,
            ResourceType = ResourceType ?? context.Controller.GetType().Name,
            ResourceId = context.ActionArguments.TryGetValue("id", out var id)
                ? id?.ToString()
                : null,
            Success = result.Exception is null,
            FailureReason = result.Exception?.Message,
            // ... populate from HttpContext
        };

        await auditService.LogAsync(auditEvent);
    }
}

Storage Strategies

Immutability Patterns

Immutability Options:
+------------------------------------------------------------------+
| Strategy              | How It Works                             |
+-----------------------+------------------------------------------+
| Append-only table     | No UPDATE/DELETE permissions on table    |
| WORM storage          | Azure Immutable Blob, AWS S3 Object Lock |
| Blockchain hash       | Each entry includes hash of previous     |
| Digital signatures    | Sign entries with HSM-backed key         |
| Write-ahead log       | Append to log, never modify              |
+------------------------------------------------------------------+

Database Schema

-- Append-only audit table
CREATE TABLE audit_logs (
    event_id UUID PRIMARY KEY,
    timestamp TIMESTAMPTZ NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    tenant_id UUID NOT NULL,
    user_id UUID,
    actor_type VARCHAR(50) NOT NULL,
    source VARCHAR(50) NOT NULL,
    action VARCHAR(50) NOT NULL,
    resource_type VARCHAR(100) NOT NULL,
    resource_id VARCHAR(255),
    success BOOLEAN NOT NULL,
    failure_reason TEXT,
    old_values JSONB,
    new_values JSONB,
    ip_address INET,
    user_agent TEXT,
    session_id VARCHAR(255),
    correlation_id VARCHAR(255),
    tags JSONB,

    -- Hash chain for tamper detection
    previous_hash VARCHAR(64),
    entry_hash VARCHAR(64) NOT NULL
);

-- Partitioning for performance and retention
CREATE TABLE audit_logs_y2024m01 PARTITION OF audit_logs
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- Indexes for common queries
CREATE INDEX idx_audit_tenant_time ON audit_logs (tenant_id, timestamp DESC);
CREATE INDEX idx_audit_user_time ON audit_logs (user_id, timestamp DESC);
CREATE INDEX idx_audit_resource ON audit_logs (resource_type, resource_id);
CREATE INDEX idx_audit_event_type ON audit_logs (event_type);

-- Revoke modification permissions
REVOKE UPDATE, DELETE ON audit_logs FROM app_user;

Hash Chain for Tamper Detection

public sealed class HashChainAuditStore(IDbContext db)
{
    public async Task AppendAsync(AuditEvent auditEvent, CancellationToken ct)
    {
        // Get hash of previous entry
        var previousHash = await db.AuditLogs
            .Where(a => a.TenantId == auditEvent.TenantId)
            .OrderByDescending(a => a.Timestamp)
            .Select(a => a.EntryHash)
            .FirstOrDefaultAsync(ct);

        // Compute hash of current entry including previous hash
        var entryHash = ComputeHash(auditEvent, previousHash);

        var logEntry = new AuditLogEntry
        {
            // ... map from auditEvent
            PreviousHash = previousHash,
            EntryHash = entryHash
        };

        db.AuditLogs.Add(logEntry);
        await db.SaveChangesAsync(ct);
    }

    private static string ComputeHash(AuditEvent entry, string? previousHash)
    {
        var data = JsonSerializer.Serialize(entry) + (previousHash ?? "");
        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(data));
        return Convert.ToHexString(hash);
    }

    public async Task<bool> VerifyChainIntegrityAsync(
        Guid tenantId,
        DateTimeOffset start,
        DateTimeOffset end,
        CancellationToken ct)
    {
        var entries = await db.AuditLogs
            .Where(a => a.TenantId == tenantId)
            .Where(a => a.Timestamp >= start && a.Timestamp <= end)
            .OrderBy(a => a.Timestamp)
            .ToListAsync(ct);

        string? previousHash = null;
        foreach (var entry in entries)
        {
            var expectedHash = ComputeHash(entry.ToAuditEvent(), previousHash);
            if (entry.EntryHash != expectedHash)
                return false;  // Tampering detected

            previousHash = entry.EntryHash;
        }

        return true;
    }
}

Retention Policies

Compliance-Driven Retention

Retention Requirements:
+------------------------------------------------------------------+
| Framework      | Minimum Retention | Notes                        |
+----------------+-------------------+------------------------------+
| SOC 2          | 1 year            | Auditor access required      |
| HIPAA          | 6 years           | From creation or last use    |
| GDPR           | As needed         | Minimize, but prove actions  |
| PCI DSS        | 1 year            | 3 months immediately online  |
| SOX            | 7 years           | Financial records            |
| General        | 3-7 years         | Legal/litigation hold        |
+------------------------------------------------------------------+

Retention Implementation

public sealed class AuditRetentionService(
    IDbContext db,
    IAuditArchiveStore archiveStore,
    ILogger<AuditRetentionService> logger)
{
    public async Task EnforceRetentionAsync(CancellationToken ct)
    {
        var policies = await GetRetentionPoliciesAsync(ct);

        foreach (var policy in policies)
        {
            // Archive before delete
            if (policy.ArchiveBeforeDelete)
            {
                var toArchive = await db.AuditLogs
                    .Where(a => a.EventType.StartsWith(policy.EventTypePrefix))
                    .Where(a => a.Timestamp < DateTimeOffset.UtcNow - policy.OnlineRetention)
                    .Where(a => a.Timestamp >= DateTimeOffset.UtcNow - policy.TotalRetention)
                    .ToListAsync(ct);

                await archiveStore.ArchiveAsync(toArchive, ct);
            }

            // Delete expired
            var deleted = await db.AuditLogs
                .Where(a => a.EventType.StartsWith(policy.EventTypePrefix))
                .Where(a => a.Timestamp < DateTimeOffset.UtcNow - policy.TotalRetention)
                .Where(a => !a.HasLegalHold)
                .ExecuteDeleteAsync(ct);

            logger.LogInformation(
                "Deleted {Count} audit logs for policy {Policy}",
                deleted, policy.Name);
        }
    }
}

Sensitive Data Handling

Redaction Patterns

public sealed class AuditRedactor
{
    private static readonly HashSet<string> SensitiveFields = new(StringComparer.OrdinalIgnoreCase)
    {
        "password", "secret", "token", "apikey", "api_key",
        "ssn", "social_security", "credit_card", "cvv",
        "bank_account", "routing_number"
    };

    public Dictionary<string, object?> Redact(Dictionary<string, object?> values)
    {
        var redacted = new Dictionary<string, object?>();

        foreach (var (key, value) in values)
        {
            if (SensitiveFields.Contains(key))
            {
                redacted[key] = "[REDACTED]";
            }
            else if (value is Dictionary<string, object?> nested)
            {
                redacted[key] = Redact(nested);
            }
            else
            {
                redacted[key] = value;
            }
        }

        return redacted;
    }
}

Querying and Analysis

Common Queries

public sealed class AuditQueryService(IDbContext db)
{
    // Security: Failed login attempts
    public async Task<IReadOnlyList<AuditEvent>> GetFailedLoginsAsync(
        Guid tenantId,
        TimeSpan window,
        CancellationToken ct)
    {
        return await db.AuditLogs
            .Where(a => a.TenantId == tenantId)
            .Where(a => a.EventType == "user.login_failed")
            .Where(a => a.Timestamp > DateTimeOffset.UtcNow - window)
            .OrderByDescending(a => a.Timestamp)
            .Take(100)
            .ToListAsync(ct);
    }

    // Compliance: All actions by user
    public async Task<PagedResult<AuditEvent>> GetUserActivityAsync(
        Guid tenantId,
        Guid userId,
        DateTimeOffset start,
        DateTimeOffset end,
        int page,
        int pageSize,
        CancellationToken ct)
    {
        var query = db.AuditLogs
            .Where(a => a.TenantId == tenantId)
            .Where(a => a.UserId == userId)
            .Where(a => a.Timestamp >= start && a.Timestamp <= end);

        var total = await query.CountAsync(ct);
        var items = await query
            .OrderByDescending(a => a.Timestamp)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(ct);

        return new PagedResult<AuditEvent>(items, total, page, pageSize);
    }

    // Data access: Who accessed this resource
    public async Task<IReadOnlyList<AuditEvent>> GetResourceAccessAsync(
        string resourceType,
        string resourceId,
        TimeSpan window,
        CancellationToken ct)
    {
        return await db.AuditLogs
            .Where(a => a.ResourceType == resourceType)
            .Where(a => a.ResourceId == resourceId)
            .Where(a => a.Timestamp > DateTimeOffset.UtcNow - window)
            .OrderByDescending(a => a.Timestamp)
            .ToListAsync(ct);
    }
}

Best Practices

Audit Logging Best Practices:
+------------------------------------------------------------------+
| Practice                  | Rationale                            |
+---------------------------+--------------------------------------+
| Log synchronously         | Ensure log before action completes   |
| Include context           | Who, what, when, where, why          |
| Use structured format     | Enables analysis and alerting        |
| Redact sensitive data     | PII, credentials, PHI protection     |
| Partition by time         | Performance and retention management |
| Hash chain for integrity  | Tamper detection                     |
| Separate storage          | Isolation from app DB                |
| Real-time alerting        | Security event detection             |
+------------------------------------------------------------------+

Anti-Patterns

Anti-PatternProblemSolution
Log after actionMay miss failuresLog before/during action
Mutable storageTampering possibleAppend-only, WORM storage
No tenant isolationData leakageInclude tenant in all queries
Plain text secretsCompliance violationAlways redact sensitive fields
Single partitionPerformance degradesTime-based partitioning
No retention policyStorage costs, legal riskDefine and enforce policies

References

Load for detailed implementation:

  • references/immutable-logs.md - Storage patterns for immutability
  • references/retention-policies.md - Compliance-driven retention

Related Skills

  • saas-compliance-frameworks - Compliance requirements
  • tenant-data-isolation - Multi-tenant audit separation

MCP Research

For current audit logging patterns:

perplexity: "audit logging compliance 2024" "immutable audit trail patterns"
microsoft-learn: "Azure Monitor audit" "Application Insights audit logging"