CRUMB a card from devarno-cloud

AuditTrail & Selective Disclosure

aegis intermediate 5 min read

ELI5

The audit trail is a security camera roll: every access gets filmed in order. By default the footage is sealed. A compliance officer can unseal specific clips (selective disclosure) — the unsealed copies go into a public folder while the originals stay in the vault.

Technical Deep Dive

AuditTrail in src/audit_trail.rs maintains two collections:

  • entries: Vec<AuditEntry> — append-only log of every access
  • disclosed_entries: HashMap<String, AuditEntry> — subset explicitly disclosed

AuditEntry

FieldValue
entry_id"{user_id}_{doc_id}_{action}_{timestamp_ms}"
user_idAs supplied
doc_idAs supplied
actionAs supplied
timestamp_msWall clock at construction
disclosedfalse at construction

entry_id is deterministic and non-random. Duplicate (user_id, doc_id, action, timestamp_ms) tuples produce the same id.

Selective Disclosure

sequenceDiagram
participant Caller
participant AuditTrail
Caller->>AuditTrail: record_access("u1", "doc1", "read")
AuditTrail->>AuditTrail: entries.push(AuditEntry)
Caller->>AuditTrail: disclose_entry(entry_id)
AuditTrail->>AuditTrail: entry.mark_disclosed() → disclosed = true
AuditTrail->>AuditTrail: disclosed_entries.insert(entry_id, entry.clone())
AuditTrail-->>Caller: Ok(())

disclose_entry does not remove the entry from entries — it marks it in-place and inserts a clone into disclosed_entries. The original entry in entries also gets disclosed = true after the iter_mut().find().

Chronological Integrity Check

flowchart LR
A[verify_trail] --> B{entries empty?}
B -- yes --> pass([true])
B -- no --> C[iterate entries]
C --> D{entry.timestamp_ms < prev_time?}
D -- yes --> fail([false])
D -- no --> E[prev_time = entry.timestamp_ms]
E --> C
C --> pass

verify_trail checks monotone non-decreasing timestamps. Same-millisecond entries pass (equal is allowed).

Stats

AuditStats.disclosed_percentage = (disclosed / total) * 100.0. Returns 0.0 if entries is empty.

Key Terms

  • AuditEntry → Per-access record with deterministic entry_id; defined in src/audit_trail.rs
  • selective disclosure → Explicitly publish a subset of entries via disclose_entry
  • disclosed_entries → HashMap clone of entries that have been disclosed
  • verify_trail → Monotone timestamp check; fails if any entry precedes its predecessor
  • mark_disclosed → Mutates AuditEntry.disclosed flag to true

Q&A

Q: Two record_access calls with identical arguments in the same millisecond — do they produce the same entry_id? A: Yes. entry_id = format!("{user_id}_{doc_id}_{action}_{now_ms}") is deterministic. Both entries sit in entries with the same id, but disclose_entry only finds the first match via iter_mut().find().

Q: Does clear() also reset disclosed_entries? A: Yes. clear() calls self.entries.clear() and self.disclosed_entries.clear() — both collections are emptied.

Q: get_disclosed_entries() returns a Vec from a HashMap — is the order deterministic? A: No. HashMap::values() iteration order is not guaranteed. Callers requiring ordered output must sort by timestamp_ms after retrieval.

Examples

Disclose a specific entry and query:

let mut trail = AuditTrail::new();
trail.record_access("u1", "doc1", "read");
let entry_id = trail.entries[0].entry_id.clone();
trail.disclose_entry(&entry_id).unwrap();
assert_eq!(trail.disclosed_count(), 1);
assert!(trail.entries[0].disclosed);
let disclosed = trail.get_disclosed_entries();
assert_eq!(disclosed.len(), 1);

neighbors on the map