mielin_cli/commands/
audit.rs

1//! Audit command implementations
2
3use crate::audit::{AuditEventType, AuditLogger, AuditSeverity};
4use crate::output::OutputFormat;
5use anyhow::Result;
6use clap::{Args, Subcommand};
7use comfy_table::{presets::UTF8_FULL, Cell, Color, ContentArrangement, Table};
8use std::path::PathBuf;
9
10/// Audit commands
11#[derive(Debug, Args)]
12pub struct AuditCommand {
13    #[command(subcommand)]
14    command: AuditSubcommand,
15}
16
17#[derive(Debug, Subcommand)]
18enum AuditSubcommand {
19    /// Show audit log entries
20    #[command(visible_aliases = &["list", "ls"])]
21    Show {
22        /// Number of entries to show
23        #[arg(short = 'n', long, default_value = "50")]
24        limit: usize,
25
26        /// Filter by event type
27        #[arg(short = 't', long)]
28        event_type: Option<String>,
29
30        /// Filter by severity (info, warning, error, critical)
31        #[arg(short = 's', long)]
32        severity: Option<String>,
33
34        /// Filter by user
35        #[arg(short = 'u', long)]
36        user: Option<String>,
37    },
38
39    /// Query audit logs with advanced filters
40    #[command(visible_aliases = &["search", "find"])]
41    Query {
42        /// Search pattern (searches in command and args)
43        pattern: String,
44
45        /// Number of entries to show
46        #[arg(short = 'n', long, default_value = "100")]
47        limit: usize,
48
49        /// Show only failed commands
50        #[arg(long)]
51        failed_only: bool,
52    },
53
54    /// Show audit statistics
55    #[command(visible_aliases = &["statistics", "info"])]
56    Stats,
57
58    /// Clear audit logs
59    #[command(visible_aliases = &["clean", "purge"])]
60    Clear {
61        /// Skip confirmation prompt
62        #[arg(short = 'y', long)]
63        yes: bool,
64    },
65
66    /// Export audit logs to file
67    #[command(visible_aliases = &["save", "dump"])]
68    Export {
69        /// Output file path
70        path: PathBuf,
71
72        /// Export format (json, csv)
73        #[arg(short = 'f', long, default_value = "json")]
74        format: String,
75    },
76}
77
78impl AuditCommand {
79    pub async fn execute(&self, output_format: OutputFormat) -> Result<()> {
80        match &self.command {
81            AuditSubcommand::Show {
82                limit,
83                event_type,
84                severity,
85                user,
86            } => {
87                show_command(
88                    *limit,
89                    event_type.as_deref(),
90                    severity.as_deref(),
91                    user.as_deref(),
92                    output_format,
93                )
94                .await
95            }
96            AuditSubcommand::Query {
97                pattern,
98                limit,
99                failed_only,
100            } => query_command(pattern, *limit, *failed_only, output_format).await,
101            AuditSubcommand::Stats => stats_command(output_format).await,
102            AuditSubcommand::Clear { yes } => clear_command(*yes).await,
103            AuditSubcommand::Export { path, format } => export_command(path, format).await,
104        }
105    }
106}
107
108async fn show_command(
109    limit: usize,
110    event_type: Option<&str>,
111    severity: Option<&str>,
112    user: Option<&str>,
113    format: OutputFormat,
114) -> Result<()> {
115    let logger = AuditLogger::with_default_config()?;
116
117    // Parse event type
118    let event_type_filter = event_type.and_then(|et| match et.to_lowercase().as_str() {
119        "command" | "cmd" => Some(AuditEventType::CommandExecution),
120        "config" | "cfg" => Some(AuditEventType::ConfigChange),
121        "auth" | "authentication" => Some(AuditEventType::Authentication),
122        "authz" | "authorization" => Some(AuditEventType::Authorization),
123        "security" | "sec" => Some(AuditEventType::Security),
124        "system" | "sys" => Some(AuditEventType::System),
125        _ => None,
126    });
127
128    // Parse severity
129    let severity_filter = severity.and_then(|sev| match sev.to_lowercase().as_str() {
130        "info" => Some(AuditSeverity::Info),
131        "warning" | "warn" => Some(AuditSeverity::Warning),
132        "error" | "err" => Some(AuditSeverity::Error),
133        "critical" | "crit" => Some(AuditSeverity::Critical),
134        _ => None,
135    });
136
137    let entries = logger.query_entries(
138        event_type_filter,
139        severity_filter,
140        user,
141        None,
142        None,
143        Some(limit),
144    )?;
145
146    if entries.is_empty() {
147        println!("No audit entries found");
148        return Ok(());
149    }
150
151    match format {
152        OutputFormat::Json => {
153            println!("{}", serde_json::to_string_pretty(&entries)?);
154        }
155        OutputFormat::Yaml => {
156            println!("{}", serde_yaml::to_string(&entries)?);
157        }
158        OutputFormat::Quiet => {
159            for entry in &entries {
160                println!("{}", entry.command);
161            }
162        }
163        OutputFormat::Table => {
164            let mut table = Table::new();
165            table
166                .load_preset(UTF8_FULL)
167                .set_content_arrangement(ContentArrangement::Dynamic)
168                .set_header(vec![
169                    "Timestamp",
170                    "User",
171                    "Command",
172                    "Type",
173                    "Severity",
174                    "Exit",
175                ]);
176
177            for entry in &entries {
178                let severity_cell = match entry.severity {
179                    AuditSeverity::Critical => {
180                        Cell::new(format!("{:?}", entry.severity)).fg(Color::Red)
181                    }
182                    AuditSeverity::Error => {
183                        Cell::new(format!("{:?}", entry.severity)).fg(Color::Red)
184                    }
185                    AuditSeverity::Warning => {
186                        Cell::new(format!("{:?}", entry.severity)).fg(Color::Yellow)
187                    }
188                    AuditSeverity::Info => {
189                        Cell::new(format!("{:?}", entry.severity)).fg(Color::Green)
190                    }
191                };
192
193                let exit_cell = match entry.exit_code {
194                    Some(0) => Cell::new("0").fg(Color::Green),
195                    Some(code) => Cell::new(format!("{}", code)).fg(Color::Red),
196                    None => Cell::new("-"),
197                };
198
199                let command_str = if entry.args.is_empty() {
200                    entry.command.clone()
201                } else {
202                    format!("{} {}", entry.command, entry.args.join(" "))
203                };
204
205                table.add_row(vec![
206                    Cell::new(entry.timestamp.format("%Y-%m-%d %H:%M:%S")),
207                    Cell::new(&entry.user),
208                    Cell::new(command_str),
209                    Cell::new(format!("{:?}", entry.event_type)),
210                    severity_cell,
211                    exit_cell,
212                ]);
213            }
214
215            println!("{}", table);
216            println!("\nShowing {} of total audit entries", entries.len());
217        }
218    }
219
220    Ok(())
221}
222
223async fn query_command(
224    pattern: &str,
225    limit: usize,
226    failed_only: bool,
227    format: OutputFormat,
228) -> Result<()> {
229    let logger = AuditLogger::with_default_config()?;
230    let mut entries = logger.read_entries()?;
231
232    // Filter by pattern
233    entries.retain(|entry| {
234        let command_str = format!("{} {}", entry.command, entry.args.join(" "));
235        command_str.to_lowercase().contains(&pattern.to_lowercase())
236    });
237
238    // Filter by failed only
239    if failed_only {
240        entries.retain(|entry| entry.exit_code.is_some() && entry.exit_code != Some(0));
241    }
242
243    // Sort by timestamp (newest first)
244    entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
245
246    // Limit
247    entries.truncate(limit);
248
249    if entries.is_empty() {
250        println!("No matching audit entries found for pattern: {}", pattern);
251        return Ok(());
252    }
253
254    match format {
255        OutputFormat::Json => {
256            println!("{}", serde_json::to_string_pretty(&entries)?);
257        }
258        OutputFormat::Yaml => {
259            println!("{}", serde_yaml::to_string(&entries)?);
260        }
261        OutputFormat::Quiet => {
262            for entry in &entries {
263                println!("{}", entry.command);
264            }
265        }
266        OutputFormat::Table => {
267            let mut table = Table::new();
268            table
269                .load_preset(UTF8_FULL)
270                .set_content_arrangement(ContentArrangement::Dynamic)
271                .set_header(vec!["Timestamp", "Command", "Exit", "Duration"]);
272
273            for entry in &entries {
274                let exit_cell = match entry.exit_code {
275                    Some(0) => Cell::new("0").fg(Color::Green),
276                    Some(code) => Cell::new(format!("{}", code)).fg(Color::Red),
277                    None => Cell::new("-"),
278                };
279
280                let duration_str = entry
281                    .duration_ms
282                    .map(|ms| format!("{}ms", ms))
283                    .unwrap_or_else(|| "-".to_string());
284
285                let command_str = if entry.args.is_empty() {
286                    entry.command.clone()
287                } else {
288                    format!("{} {}", entry.command, entry.args.join(" "))
289                };
290
291                table.add_row(vec![
292                    Cell::new(entry.timestamp.format("%Y-%m-%d %H:%M:%S")),
293                    Cell::new(command_str),
294                    exit_cell,
295                    Cell::new(duration_str),
296                ]);
297            }
298
299            println!("{}", table);
300            println!("\nFound {} matching entries", entries.len());
301        }
302    }
303
304    Ok(())
305}
306
307async fn stats_command(format: OutputFormat) -> Result<()> {
308    let logger = AuditLogger::with_default_config()?;
309    let stats = logger.get_stats()?;
310
311    match format {
312        OutputFormat::Json => {
313            println!("{}", serde_json::to_string_pretty(&stats)?);
314        }
315        OutputFormat::Yaml => {
316            println!("{}", serde_yaml::to_string(&stats)?);
317        }
318        OutputFormat::Quiet => {
319            println!("{}", stats.total_entries);
320        }
321        OutputFormat::Table => {
322            println!("Audit Log Statistics");
323            println!("====================\n");
324
325            println!("Total Entries: {}", stats.total_entries);
326
327            if let (Some(oldest), Some(newest)) = (stats.oldest_entry, stats.newest_entry) {
328                println!(
329                    "Date Range: {} to {}",
330                    oldest.format("%Y-%m-%d %H:%M:%S"),
331                    newest.format("%Y-%m-%d %H:%M:%S")
332                );
333            }
334
335            println!("\nBy Event Type:");
336            for (event_type, count) in &stats.by_event_type {
337                println!("  {}: {}", event_type, count);
338            }
339
340            println!("\nBy Severity:");
341            for (severity, count) in &stats.by_severity {
342                println!("  {}: {}", severity, count);
343            }
344
345            println!("\nBy User:");
346            for (user, count) in &stats.by_user {
347                println!("  {}: {}", user, count);
348            }
349        }
350    }
351
352    Ok(())
353}
354
355async fn clear_command(yes: bool) -> Result<()> {
356    if !yes {
357        print!("Are you sure you want to clear all audit logs? [y/N] ");
358        std::io::Write::flush(&mut std::io::stdout())?;
359
360        let mut input = String::new();
361        std::io::stdin().read_line(&mut input)?;
362
363        if !input.trim().eq_ignore_ascii_case("y") {
364            println!("Aborted");
365            return Ok(());
366        }
367    }
368
369    let logger = AuditLogger::with_default_config()?;
370    logger.clear()?;
371
372    println!("✓ Audit logs cleared successfully");
373
374    Ok(())
375}
376
377async fn export_command(path: &PathBuf, format: &str) -> Result<()> {
378    let logger = AuditLogger::with_default_config()?;
379    let entries = logger.read_entries()?;
380
381    let content = match format.to_lowercase().as_str() {
382        "json" => serde_json::to_string_pretty(&entries)?,
383        "csv" => {
384            let mut csv = String::from(
385                "timestamp,user,command,args,exit_code,duration_ms,event_type,severity\n",
386            );
387            for entry in &entries {
388                csv.push_str(&format!(
389                    "{},{},{},{},{},{},{:?},{:?}\n",
390                    entry.timestamp.to_rfc3339(),
391                    entry.user,
392                    entry.command,
393                    entry.args.join(" "),
394                    entry.exit_code.map(|c| c.to_string()).unwrap_or_default(),
395                    entry.duration_ms.map(|d| d.to_string()).unwrap_or_default(),
396                    entry.event_type,
397                    entry.severity,
398                ));
399            }
400            csv
401        }
402        _ => anyhow::bail!("Unsupported export format: {}. Use 'json' or 'csv'", format),
403    };
404
405    std::fs::write(path, content)?;
406
407    println!(
408        "✓ Exported {} audit entries to {}",
409        entries.len(),
410        path.display()
411    );
412
413    Ok(())
414}
415
416/// Handle audit command
417pub async fn handle_audit_command(command: AuditCommand, format: OutputFormat) -> Result<()> {
418    command.execute(format).await
419}