1use anyhow::Context;
2use owo_colors::OwoColorize;
3
4pub(crate) fn cmd_audit_verify() -> anyhow::Result<()> {
5 let audit_path = core_config::config_dir().join("audit.jsonl");
6
7 if !audit_path.exists() {
8 println!("{}", "No audit log found.".dimmed());
9 return Ok(());
10 }
11
12 let contents = std::fs::read_to_string(&audit_path).context("failed to read audit log")?;
13
14 match core_profile::verify_chain(&contents, &core_types::AuditHash::Blake3) {
15 Ok(count) => {
16 println!("{} {} entries verified.", "OK:".green().bold(), count);
17 }
18 Err(e) => {
19 eprintln!(
20 "{} audit chain integrity check failed: {e}",
21 "FAIL:".red().bold()
22 );
23 std::process::exit(1);
24 }
25 }
26
27 Ok(())
28}
29
30pub(crate) async fn cmd_audit_tail(count: usize, follow: bool) -> anyhow::Result<()> {
31 let audit_path = core_config::config_dir().join("audit.jsonl");
32
33 if !audit_path.exists() {
34 println!("{}", "No audit log found.".dimmed());
35 return Ok(());
36 }
37
38 let contents = std::fs::read_to_string(&audit_path).context("failed to read audit log")?;
39
40 let lines: Vec<&str> = contents.lines().filter(|l| !l.trim().is_empty()).collect();
41 let start = lines.len().saturating_sub(count);
42
43 for line in &lines[start..] {
44 print_audit_entry(line);
45 }
46
47 if !follow {
48 return Ok(());
49 }
50
51 let mut last_len = std::fs::metadata(&audit_path).map(|m| m.len()).unwrap_or(0);
53
54 let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(4);
55
56 let watch_path = audit_path.clone();
57 let _watcher = {
58 use notify::{EventKind as NotifyEvent, RecommendedWatcher, RecursiveMode, Watcher};
59
60 let mut watcher = RecommendedWatcher::new(
61 move |res: Result<notify::Event, notify::Error>| {
62 if let Ok(event) = res
63 && matches!(event.kind, NotifyEvent::Modify(_))
64 {
65 let _ = tx.blocking_send(());
66 }
67 },
68 notify::Config::default(),
69 )
70 .context("failed to start file watcher")?;
71
72 watcher
73 .watch(
74 watch_path.parent().unwrap_or(watch_path.as_ref()),
75 RecursiveMode::NonRecursive,
76 )
77 .context("failed to watch audit log directory")?;
78
79 watcher
80 };
81
82 loop {
83 tokio::select! {
84 Some(()) = rx.recv() => {
85 let new_len = std::fs::metadata(&audit_path)
86 .map(|m| m.len())
87 .unwrap_or(0);
88
89 if new_len > last_len {
90 use std::io::{Read, Seek, SeekFrom};
92 let mut f = std::fs::File::open(&audit_path)?;
93 f.seek(SeekFrom::Start(last_len))?;
94 let mut buf = String::new();
95 f.read_to_string(&mut buf)?;
96 last_len = new_len;
97
98 for line in buf.lines() {
99 if !line.trim().is_empty() {
100 print_audit_entry(line);
101 }
102 }
103 }
104 }
105 _ = tokio::signal::ctrl_c() => {
106 break;
107 }
108 }
109 }
110
111 Ok(())
112}
113
114fn print_audit_entry(line: &str) {
115 if let Ok(entry) = serde_json::from_str::<serde_json::Value>(line)
116 && let Ok(pretty) = serde_json::to_string_pretty(&entry)
117 {
118 println!("{pretty}");
119 println!("---");
120 return;
121 }
122 println!("{line}");
123}