| 1 | //! Lightweight audit logging for sensitive operations. |
| 2 | |
| 3 | use std::fs; |
| 4 | use std::path::PathBuf; |
| 5 | |
| 6 | use chrono::Utc; |
| 7 | use serde_json::{Value, json}; |
| 8 | |
| 9 | use crate::utils::{flush_and_sync, open_append}; |
| 10 | |
| 11 | /// Append an audit event to `$CODEWHALE_HOME/audit.log` (or the default |
| 12 | /// `~/.codewhale/audit.log` when no explicit CodeWhale home is configured). |
| 13 | /// |
| 14 | /// This helper is best-effort by design: callers should not fail critical flows |
| 15 | /// if audit persistence fails. |
| 16 | pub fn log_sensitive_event(event: &str, details: Value) { |
| 17 | if let Err(err) = append_event(event, details) { |
| 18 | crate::logging::warn(format!("audit log write failed: {err}")); |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | fn append_event(event: &str, details: Value) -> anyhow::Result<()> { |
| 23 | let path = default_audit_path()?; |
| 24 | let parent = path.parent().map(|p| p.to_path_buf()); |
| 25 | if let Some(ref parent) = parent { |
| 26 | fs::create_dir_all(parent)?; |
| 27 | } |
| 28 | // Open for append with a BufWriter for buffered I/O, then flush + fsync |
| 29 | // after each event so the record is durably on disk. |
| 30 | let mut writer = open_append(&path)?; |
| 31 | let record = json!({ |
| 32 | "ts": Utc::now().to_rfc3339(), |
| 33 | "event": event, |
| 34 | "details": details, |
| 35 | }); |
| 36 | let line = serde_json::to_string(&record)?; |
| 37 | use std::io::Write; |
| 38 | writeln!(writer, "{line}")?; |
| 39 | flush_and_sync(&mut writer)?; |
| 40 | Ok(()) |
| 41 | } |
| 42 | |
| 43 | fn default_audit_path() -> anyhow::Result<PathBuf> { |
| 44 | Ok(codewhale_config::codewhale_home()?.join("audit.log")) |
| 45 | } |
| 46 |