//! Audit log record types. One JSON object per line, hash-chained by the exact bytes of each line. use serde::{Deserialize, Serialize}; use crate::{CallId, DataClass, DenyReason, Hash32, SessionId, Timestamp}; pub use crate::chain::{ChainFailure, ChainReport, ChainVerifier, Location, TornTail}; // JSON: {"outcome":"denied","reason":"no_grant"} ; the tag sits beside the fields #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)] pub enum DecisionRecord { Allowed {}, Ask {}, Denied { reason: DenyReason }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ApprovalAnswer { Approved, Refused, Expired, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ResultStatus { Result, Failed, } // JSON: {"type":"decision","session":"…",…} ; the tag sits beside the fields #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] pub enum AuditEvent { Decision { session: SessionId, call: CallId, tool: String, arguments: String, outcome: DecisionRecord, grant: Option, grant_sha256: Option, taint: DataClass, untrusted: bool, }, Approval { session: SessionId, call: CallId, decision: u64, answer: ApprovalAnswer, by: Option, post: Option, reason: Option, outcome: DecisionRecord, grant: Option, grant_sha256: Option, taint: DataClass, untrusted: bool, }, Result { session: SessionId, call: CallId, decision: u64, status: ResultStatus, class: DataClass, untrusted: bool, truncated: bool, bytes: u64, sha256: Hash32, taint_after: DataClass, }, Recovery { torn_bytes: u64, torn_sha256: Hash32, }, AcceptedBreak { file: String, line: u64, last_good: Hash32, }, } // JSON: {"seq":0,"time":"…","prev":"…","event":{"type":"decision",…}} ; `event` is a nested object. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AuditRecord { pub seq: u64, pub time: Timestamp, pub prev: Hash32, pub event: AuditEvent, } /// True if `name` is an audit log file: `YYYY-MM-DD.jsonl`, ASCII digits with `-` at positions 4 /// and 7, a month from 01 to 12 and a day from 01 to 31. `brokerd` and `bxctl audit verify` both /// decide with this, so they read the same files. pub fn is_audit_log_name(name: &str) -> bool { let Some(date) = name.strip_suffix(".jsonl") else { return false; }; let shaped = date.len() == 10 && date.bytes().enumerate().all(|(i, b)| match i { 4 | 7 => b == b'-', _ => b.is_ascii_digit(), }); if !shaped { return false; } let number = |range: std::ops::Range| date.get(range).and_then(|s| s.parse::().ok()); matches!(number(5..7), Some(1..=12)) && matches!(number(8..10), Some(1..=31)) }