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 accessdisclosed_entries: HashMap<String, AuditEntry>— subset explicitly disclosed
AuditEntry
| Field | Value |
|---|---|
entry_id | "{user_id}_{doc_id}_{action}_{timestamp_ms}" |
user_id | As supplied |
doc_id | As supplied |
action | As supplied |
timestamp_ms | Wall clock at construction |
disclosed | false 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 --> passverify_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 insrc/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.disclosedflag totrue
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
- FNP Observability & Prometheus Metrics monitoring FNP systems
- FNP End-to-End Encryption & Zero Trust Architecture understanding FNP's security layers
- Onboarding Lifecycle Events wiring an analytics consumer to onboarding signals
- JWT Auth & RBAC Hierarchy adding a new permission to a tool