Skip to main content

zoi_cli/cmd/
history.rs

1//! Command for viewing and verifying the audit history of Zoi operations.
2
3use std::path::PathBuf;
4
5use anyhow::{Result, anyhow};
6use colored::Colorize;
7use comfy_table::presets::UTF8_FULL;
8use comfy_table::{Cell, Color, ContentArrangement, Table};
9
10use crate::pkg::audit;
11
12/// Runs the 'history' command.
13///
14/// Displays the audit history in a table, exports it to a file,
15/// or verifies the cryptographic integrity of the history chain.
16///
17/// # Errors
18///
19/// Returns an error if the history cannot be retrieved, exported, or verified.
20pub fn run(verify: bool, export: Option<PathBuf>, ndjson: bool) -> Result<()> {
21    if verify {
22        let report = audit::verify_chain()?;
23        if report.valid {
24            println!(
25                "{} {} (entries: {}, chained: {}, legacy: {})",
26                "::".bold().blue(),
27                report.message.green(),
28                report.total_entries,
29                report.hashed_entries,
30                report.legacy_entries
31            );
32            return Ok(());
33        }
34        return Err(anyhow!(report.message));
35    }
36
37    if let Some(path) = export {
38        let total = audit::export_history(&path, ndjson)?;
39        println!(
40            "{} Exported {} audit entr{} to {} (format: {}).",
41            "::".bold().green(),
42            total,
43            if total == 1 { "y" } else { "ies" },
44            path.display().to_string().cyan(),
45            if ndjson { "ndjson" } else { "json" }
46        );
47        return Ok(());
48    }
49
50    println!("{} Zoi operation history...", "::".bold().blue());
51
52    let history = audit::get_history()?;
53
54    if history.is_empty() {
55        println!("No history recorded. Audit logging might be disabled.");
56        return Ok(());
57    }
58
59    let mut table = Table::new();
60    table
61        .load_style(UTF8_FULL)
62        .set_content_arrangement(ContentArrangement::Dynamic)
63        .set_header(vec![
64            "Date/Time",
65            "User",
66            "Action",
67            "Package",
68            "Version",
69            "Repo",
70            "Type",
71            "Scope",
72        ]);
73
74    for entry in history {
75        let action_cell = match entry.action {
76            audit::AuditAction::Install => {
77                Cell::new("Install").fg(Color::Green)
78            }
79            audit::AuditAction::Uninstall => {
80                Cell::new("Uninstall").fg(Color::Red)
81            }
82            audit::AuditAction::Upgrade => {
83                Cell::new("Upgrade").fg(Color::Yellow)
84            }
85        };
86
87        table.add_row(vec![
88            Cell::new(
89                entry
90                    .timestamp
91                    .with_timezone(&chrono::Local)
92                    .format("%Y-%m-%d %H:%M:%S")
93            ),
94            Cell::new(entry.user),
95            action_cell,
96            Cell::new(entry.package_name).fg(Color::Cyan),
97            Cell::new(entry.version),
98            Cell::new(entry.repo),
99            Cell::new(format!("{:?}", entry.package_type)),
100            Cell::new(format!("{:?}", entry.scope)),
101        ]);
102    }
103
104    println!("{table}");
105
106    Ok(())
107}