Audit & Compliance Guide

Comprehensive guide to audit logging and compliance in Armature.

Table of Contents


Overview

Armature's audit module provides comprehensive audit logging for security, compliance, and operational tracking. It's designed for enterprise applications that need SOC2, PCI-DSS, GDPR, or HIPAA compliance.

Key Differences from Application Logging:

Application Logging Audit Logging
Debugging & monitoring Compliance & security
Can be lost/rotated Durable, append-only records
General events Who did what, when
Verbose Structured
Optional Required for compliance

Durability vs. immutability: [FileBackend] fsyncs every event before write returns, so a record is on disk before the call that produced it completes โ€” a crash cannot lose an event that was successfully logged. It is not tamper-evident, though: the log is a plain JSON-lines file with no hash chaining, signing, or checksums, and [RetentionManager]'s pruning rewrites the file in place to drop expired lines. Anyone with filesystem access to the log can edit or delete entries without detection. If your compliance regime requires a cryptographically tamper-evident trail, pair this crate with an external WORM store, a hash-chained/signed log shipper, or an immutable object-storage backend (e.g. S3 Object Lock) fed from [MultiBackend] โ€” this crate does not provide that guarantee on its own.


Features

  • โœ… Structured Audit Events - Who, what, when, where tracking
  • โœ… Automatic HTTP Logging - Request/response middleware
  • โœ… Data Masking - PII, passwords, credit cards, SSN
  • โœ… Multiple Backends - File, memory, stdout, extensible
  • โœ… Retention Policies - Automatic cleanup with configurable TTL
  • โœ… Compliance Ready - PCI-DSS, GDPR, SOC2, HIPAA
  • โœ… Async & Thread-safe - Production-ready performance

Quick Start

1. Add Dependency

[dependencies]
armature-audit = "0.1"

2. Create Audit Logger

use armature_audit::*;
use std::sync::Arc;

let logger = Arc::new(
    AuditLogger::builder()
        .backend(FileBackend::new("audit.log"))
        .build()
);

3. Log Events

logger.log(AuditEvent::new("user.login")
    .user("alice")
    .ip("192.168.1.100")
    .action("authenticate")
    .status(AuditStatus::Success)).await?;

4. Add Middleware (Optional)

use armature_core::*;

let audit_middleware = Arc::new(AuditMiddleware::new(logger));

let app = Application::new()
    .middleware(audit_middleware)
    .build();

Audit Events

Event Structure

pub struct AuditEvent {
    id: String,                    // Unique event ID (UUID)
    timestamp: DateTime<Utc>,      // When it occurred
    event_type: String,            // Event type (e.g., "user.login")
    user_id: Option<String>,       // Who performed the action
    ip_address: Option<String>,    // From where
    user_agent: Option<String>,    // User's browser/client
    resource_type: Option<String>, // What was accessed
    resource_id: Option<String>,   // Specific resource
    action: String,                // What was done
    status: AuditStatus,           // Success/Failure/Denied/Error
    severity: AuditSeverity,       // Info/Warning/Error/Critical
    method: Option<String>,        // HTTP method
    path: Option<String>,          // Request path
    status_code: Option<u16>,      // HTTP status
    metadata: HashMap<...>,        // Custom fields
    error: Option<String>,         // Error message
    request_body: Option<String>,  // Request payload (masked)
    response_body: Option<String>, // Response payload (masked)
    duration_ms: Option<u64>,      // Duration
}

Creating Events

use armature_audit::*;

// Basic event
let event = AuditEvent::new("user.login");

// Complete event
let event = AuditEvent::new("resource.update")
    .user("alice")
    .ip("192.168.1.100")
    .user_agent("Mozilla/5.0...")
    .resource("document")
    .resource_id("doc_123")
    .action("update")
    .status(AuditStatus::Success)
    .severity(AuditSeverity::Info)
    .method("PUT")
    .path("/api/documents/123")
    .status_code(200)
    .metadata("fields_changed", serde_json::json!(["title", "content"]))
    .duration_ms(150);

Event Types

Use dot notation for hierarchy:

// Authentication
"user.login"
"user.logout"
"user.login.failed"

// Resource operations
"resource.create"
"resource.read"
"resource.update"
"resource.delete"

// Administrative
"admin.user.created"
"admin.role.assigned"
"admin.config.changed"

// Compliance
"gdpr.data_access"
"gdpr.data_export"
"pci.payment.processed"

Status Values

pub enum AuditStatus {
    Success,  // Operation succeeded
    Failure,  // Operation failed
    Denied,   // Operation denied (authorization)
    Error,    // Operation resulted in error
}

Severity Levels

pub enum AuditSeverity {
    Info,      // Informational
    Warning,   // Warrants attention
    Error,     // Error condition
    Critical,  // Critical security/compliance event
}

Storage Backends

FileBackend

Writes events to a file (one JSON object per line).

use armature_audit::*;

let backend = FileBackend::new("audit.log");

let logger = AuditLogger::builder()
    .backend(backend)
    .build();

Format: JSON Lines (JSONL)

{"id":"...","timestamp":"...","event_type":"user.login",...}
{"id":"...","timestamp":"...","event_type":"resource.update",...}

MemoryBackend

Stores events in memory (for testing and querying).

let backend = MemoryBackend::new();

// Query recent events
let events = backend.read(100).await?;

// Clear events
backend.clear().await;

StdoutBackend

Prints events to stdout (for development).

let backend = StdoutBackend::new();

MultiBackend

Write to multiple backends simultaneously.

let multi = MultiBackend::new()
    .add(Box::new(FileBackend::new("audit.log")))
    .add(Box::new(MemoryBackend::new()))
    .add(Box::new(StdoutBackend::new()));

let logger = AuditLogger::builder()
    .backend(multi)
    .build();

Custom Backend

Implement the AuditBackend trait:

use armature_audit::*;
use async_trait::async_trait;

struct DatabaseBackend {
    // database connection
}

#[async_trait]
impl AuditBackend for DatabaseBackend {
    async fn write(&self, event: &AuditEvent) -> Result<(), AuditBackendError> {
        // Write to database
        Ok(())
    }

    async fn flush(&self) -> Result<(), AuditBackendError> {
        Ok(())
    }
}

Data Masking

Default Masking

Automatically masks common sensitive fields:

password, secret, token, api_key, credit_card, cvv, ssn, private_key

Masking Configuration

use armature_audit::*;

let config = MaskingConfig::new()
    .add_field("custom_field")
    .mask_emails(true)
    .mask_phones(true)
    .mask_ssn(true)
    .mask_credit_cards(true)
    .mask_char('*')
    .show_last_chars(4);

let logger = AuditLogger::builder()
    .masking_config(config)
    .build();

What Gets Masked

Passwords & Tokens:

// Before
{"username": "alice", "password": "secret123"}

// After
{"username": "alice", "password": "******123"}

Email Addresses:

Before: Email: user@example.com
After:  Email: [EMAIL]

Phone Numbers:

Before: Phone: 123-456-7890
After:  Phone: [PHONE]

Credit Cards:

Before: Card: 4532-1234-5678-9010
After:  Card: [CARD]

SSN:

Before: SSN: 123-45-6789
After:  SSN: [SSN]

Nested JSON Masking

let data = serde_json::json!({
    "user": {
        "name": "Alice",
        "credentials": {
            "password": "secret123",
            "api_key": "key_abc123"
        }
    }
});

let masked = mask_json(&data, &config);
// credentials.password and credentials.api_key are masked

Request Logging Middleware

Basic Usage

use armature_audit::*;
use armature_core::*;
use std::sync::Arc;

let logger = Arc::new(AuditLogger::builder()
    .backend(FileBackend::new("audit.log"))
    .build());

let audit_middleware = Arc::new(AuditMiddleware::new(logger));

let app = Application::new()
    .middleware(audit_middleware)
    .build();

Configuration

let audit_middleware = Arc::new(
    AuditMiddleware::new(logger)
        .log_request_body(true)      // Log request bodies
        .log_response_body(true)     // Log response bodies
        .max_body_size(10_000)       // Max 10KB body size
        .fail_on_error(false)        // Default: fail open on audit-write errors.
                                      // Set true to fail the request closed instead.
);

An audit-write failure always increments audit_middleware.write_failure_count() regardless of fail_on_error, so it stays observable even under the default fail-open behavior.

What Gets Logged

For each HTTP request:

  • Event type: http.request
  • HTTP method (GET, POST, etc.)
  • Request path
  • Status code
  • User ID (from Authorization header)
  • IP address (from X-Forwarded-For or X-Real-IP)
  • User agent
  • Request body (masked)
  • Response body (masked)
  • Duration in milliseconds

Example Audit Log Entry

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2024-12-13T10:30:45.123Z",
  "event_type": "http.request",
  "user_id": "authenticated_user",
  "ip_address": "192.168.1.100",
  "user_agent": "Mozilla/5.0...",
  "action": "http_request",
  "status": "success",
  "severity": "info",
  "method": "POST",
  "path": "/api/users",
  "status_code": 201,
  "request_body": "{\"username\":\"alice\",\"password\":\"******123\"}",
  "response_body": "{\"id\":123,\"username\":\"alice\"}",
  "duration_ms": 45
}

Retention Policies

Configuration

use armature_audit::*;
use chrono::Duration;

// Keep logs for 90 days
let policy = RetentionPolicy::days(90);

// Keep logs for 30 days, cleanup hourly
let policy = RetentionPolicy::days(30)
    .cleanup_interval(std::time::Duration::from_secs(3600));

// Keep logs for 7 days
let policy = RetentionPolicy::days(7);

// Keep logs for 24 hours
let policy = RetentionPolicy::hours(24);

Retention Manager

use std::sync::Arc;

let backend = Arc::new(MemoryBackend::new());
let policy = RetentionPolicy::days(90);

let manager = Arc::new(RetentionManager::new(backend, policy));

// Start automatic cleanup
manager.clone().start().await;

// Manual cleanup
let deleted = manager.cleanup().await?;
println!("Deleted {} old logs", deleted);

// Stop cleanup
manager.stop().await;

Common Retention Periods

Use Case Retention Period
Development 7 days
Standard apps 30-90 days
SOC 2 1 year
PCI-DSS 1 year (3 months online)
HIPAA 6 years
GDPR As needed, with deletion capability

Compliance

PCI-DSS Compliance

For payment card data:

use armature_audit::*;

// Mask credit card data
let config = MaskingConfig::new()
    .add_field("credit_card")
    .add_field("cvv")
    .mask_credit_cards(true);

// Log payment transactions
logger.log(AuditEvent::new("payment.processed")
    .user(user_id)
    .resource("payment")
    .status(AuditStatus::Success)
    .severity(AuditSeverity::Critical)
    .metadata("amount", serde_json::json!(99.99))
    .metadata("compliance", serde_json::json!("PCI-DSS"))).await?;

// Retention: 1 year minimum
let policy = RetentionPolicy::days(365);

GDPR Compliance

For personal data:

// Mask PII
let config = MaskingConfig::new()
    .mask_emails(true)
    .add_field("ssn")
    .add_field("date_of_birth");

// Log data access
logger.log(AuditEvent::new("gdpr.data_access")
    .user(admin_id)
    .resource("user_data")
    .resource_id(user_id)
    .action("data_export")
    .metadata("purpose", serde_json::json!("user request"))
    .metadata("compliance", serde_json::json!("GDPR"))).await?;

// Support deletion (right to be forgotten)
backend.delete_user_events(user_id).await?;

SOC 2 Compliance

For security and availability:

// Log all administrative actions
logger.log(AuditEvent::new("admin.user.created")
    .user(admin_id)
    .resource("user")
    .resource_id(new_user_id)
    .action("create")
    .severity(AuditSeverity::Warning)
    .metadata("role", serde_json::json!("admin"))).await?;

// Log configuration changes
logger.log(AuditEvent::new("config.changed")
    .user(admin_id)
    .resource("system_config")
    .action("update")
    .severity(AuditSeverity::Critical)
    .metadata("setting", serde_json::json!("max_upload_size"))
    .metadata("old_value", serde_json::json!(10))
    .metadata("new_value", serde_json::json!(100))).await?;

HIPAA Compliance

For healthcare data:

// Mask PHI
let config = MaskingConfig::new()
    .add_field("ssn")
    .add_field("medical_record_number")
    .add_field("patient_id")
    .mask_emails(true);

// Log PHI access
logger.log(AuditEvent::new("phi.accessed")
    .user(doctor_id)
    .resource("medical_record")
    .resource_id(patient_id)
    .action("view")
    .severity(AuditSeverity::Warning)
    .metadata("compliance", serde_json::json!("HIPAA"))
    .metadata("purpose", serde_json::json!("treatment"))).await?;

// Retention: 6 years minimum
let policy = RetentionPolicy::days(365 * 6);

Best Practices

1. Log Meaningful Events

// โœ… Good - specific and actionable
logger.log(AuditEvent::new("user.password.reset")
    .user(user_id)
    .action("password_reset")
    .status(AuditStatus::Success)
    .metadata("method", serde_json::json!("email_link"))).await?;

// โŒ Bad - too generic
logger.log(AuditEvent::new("event")
    .action("action")).await?;

2. Use Appropriate Severity

// Info - Normal operations
logger.log(AuditEvent::new("user.profile.viewed")
    .severity(AuditSeverity::Info)).await?;

// Warning - Attention needed
logger.log(AuditEvent::new("user.login.failed")
    .severity(AuditSeverity::Warning)).await?;

// Error - System errors
logger.log(AuditEvent::new("api.error")
    .severity(AuditSeverity::Error)).await?;

// Critical - Security/compliance events
logger.log(AuditEvent::new("admin.role.assigned")
    .severity(AuditSeverity::Critical)).await?;

3. Always Log Security Events

// โœ… Always log:
- Login attempts (success and failure)
- Logout events
- Permission changes
- Administrative actions
- Data access/export
- Configuration changes
- Payment transactions
- PHI/PII access

4. Include Context

// โœ… Good - includes context
logger.log(AuditEvent::new("document.deleted")
    .user("alice")
    .resource("document")
    .resource_id("doc_123")
    .metadata("document_type", serde_json::json!("contract"))
    .metadata("reason", serde_json::json!("user request"))).await?;

// โŒ Bad - no context
logger.log(AuditEvent::new("deleted")).await?;

5. Handle Failures Gracefully

// โœ… Good - don't fail the request if audit fails, but don't lose the
// failure either: log it and count it.
if let Err(e) = logger.log(event).await {
    tracing::error!("Failed to log audit event: {}", e);
}

// โŒ Bad - failing request
logger.log(event).await?; // Request fails if audit fails!

AuditMiddleware failure handling: by default the middleware mirrors the snippet above โ€” a request still completes even if its audit event could not be durably written. That failure is not silent, though:

  • Every write failure increments AuditMiddleware::write_failure_count(), an in-process counter you can poll and export to whatever metrics system your application uses (e.g. as an audit_write_failures_total gauge).
  • A tracing::error! is emitted with the underlying backend error.

If your compliance posture requires that a request never succeed without a durable audit record, opt into fail-closed behavior instead:

let audit_middleware = Arc::new(
    AuditMiddleware::new(logger)
        .fail_on_error(true) // a failed audit write now fails the request
);

This mirrors RateLimitMiddleware::skip_on_error in armature-ratelimit (inverted default): fail_on_error defaults to false to preserve the historical fail-open behavior.


Examples

Example 1: Basic Audit Logging

use armature_audit::*;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create logger
    let logger = Arc::new(
        AuditLogger::builder()
            .backend(FileBackend::new("audit.log"))
            .build()
    );

    // Log event
    logger.log(AuditEvent::new("user.login")
        .user("alice")
        .ip("192.168.1.100")
        .status(AuditStatus::Success)).await?;

    Ok(())
}

Example 2: Request/Response Logging

use armature_core::*;
use armature_audit::*;
use std::sync::Arc;

let logger = Arc::new(AuditLogger::builder()
    .backend(FileBackend::new("audit.log"))
    .build());

let audit_middleware = Arc::new(
    AuditMiddleware::new(logger)
        .log_request_body(true)
        .log_response_body(true)
);

let app = Application::new()
    .middleware(audit_middleware)
    .build();

Example 3: Multiple Backends

let multi = MultiBackend::new()
    .add(Box::new(FileBackend::new("audit.log")))
    .add(Box::new(MemoryBackend::new()))
    .add(Box::new(StdoutBackend::new()));

let logger = AuditLogger::builder()
    .backend(multi)
    .build();

Example 4: Custom Masking

let config = MaskingConfig::new()
    .add_field("credit_card")
    .add_field("ssn")
    .add_field("medical_id")
    .mask_emails(true)
    .mask_phones(true)
    .show_last_chars(4);

let logger = AuditLogger::builder()
    .masking_config(config)
    .build();

Example 5: Retention Policy

use chrono::Duration;

let backend = Arc::new(MemoryBackend::new());
let policy = RetentionPolicy::days(90);
let manager = Arc::new(RetentionManager::new(backend, policy));

// Start automatic cleanup
manager.clone().start().await;

// Stop when done
manager.stop().await;

Summary

Key Points:

  1. Audit logs are for compliance - not debugging
  2. Use structured events - who, what, when, where
  3. Mask sensitive data - PII, passwords, credit cards
  4. Choose appropriate retention - based on compliance needs
  5. Use multiple backends - for redundancy
  6. Log security events - always
  7. Don't fail requests - if audit logging fails

Quick Reference:

// Create logger
let logger = Arc::new(AuditLogger::builder()
    .backend(FileBackend::new("audit.log"))
    .build());

// Log event
logger.log(AuditEvent::new("user.action")
    .user("alice")
    .status(AuditStatus::Success)).await?;

// Add middleware
let app = Application::new()
    .middleware(Arc::new(AuditMiddleware::new(logger)))
    .build();

// Setup retention
let manager = Arc::new(RetentionManager::new(
    backend,
    RetentionPolicy::days(90)
));
manager.clone().start().await;

Compliance Checklist:

  • โœ… Track who did what, when
  • โœ… Mask sensitive data (PII, passwords, etc.)
  • โœ… Durable, append-only-by-default audit trail (not tamper-evident โ€” see Overview)
  • โœ… Appropriate retention periods
  • โœ… Secure storage
  • โœ… Query capability
  • โœ… Deletion capability (GDPR)

Resources: