audit-trail 0.5.0

Structured audit logging with tamper-evident chaining. Every write produces a cryptographically linked record (hash chain). Compliance-grade output (who, what, when, where, result). Pluggable backends. Foundation for HIPAA, SOC 2, and PCI-DSS compliance.
Documentation

What it does

Structured audit logging with tamper-evident chaining. Every write produces a cryptographically linked record (hash chain). Compliance-grade output (who, what, when, where, result). Pluggable backends. Foundation for HIPAA, SOC 2, and PCI-DSS compliance.


Quick start

[dependencies]
audit-trail = { version = "0.5", features = ["sha2"] }
use audit_trail::{
    Action, Actor, Chain, Clock, MemorySink, Outcome, Sha256Hasher, Target, Timestamp, Verifier,
};

// Plug in any monotonic time source.
struct SystemClock;
impl Clock for SystemClock {
    fn now(&self) -> Timestamp {
        let ns = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_nanos() as u64)
            .unwrap_or(0);
        Timestamp::from_nanos(ns)
    }
}

let mut chain = Chain::new(Sha256Hasher::new(), MemorySink::new(), SystemClock);

chain.append(
    Actor::new("user-42"),
    Action::new("record.delete"),
    Target::new("record:1337"),
    Outcome::Denied,
).expect("append");

// Later, prove the chain is untampered.
let (_, sink, _) = chain.into_parts();
let mut verifier = Verifier::new(Sha256Hasher::new());
for r in sink.records() {
    verifier.verify(&r.as_record()).expect("chain must verify");
}

Persisting to a file

use audit_trail::{Chain, FileSink, FileReader, Sha256Hasher, Verifier};
# struct C; impl audit_trail::Clock for C { fn now(&self) -> audit_trail::Timestamp { audit_trail::Timestamp::from_nanos(0) } }
# let clock = C;
let sink = FileSink::open_or_create("audit.log").expect("open");
let mut chain = Chain::new(Sha256Hasher::new(), sink, clock);
// ... chain.append(...) ...

// Replay and verify the on-disk log.
let mut verifier = Verifier::new(Sha256Hasher::new());
for record in FileReader::open("audit.log").expect("open") {
    let r = record.expect("decode");
    verifier.verify(&r.as_record()).expect("verify");
}

FileSink writes a versioned 16-byte header on a fresh file, then length-prefixed records using the stable [codec] encoding. Reopening the same path appends after validating the header.

Features

Feature Default What it adds
std yes FileSink, FileReader, std::error::Error impls. Implies alloc.
alloc yes (via std) OwnedRecord, MemorySink, codec module
sha2 no Sha256Hasher (reference SHA-256 implementation)

For no_std use default-features = false and supply your own hasher, sink, and clock.


Standards

  • REPS governs every decision. See REPS.md.
  • MSRV: Rust 1.85.
  • Edition: 2024.
  • Cross-platform: Linux, macOS, Windows.

License

Dual-licensed under either of:

at your option.