1use 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#[derive(Debug, Args)]
12pub struct AuditCommand {
13 #[command(subcommand)]
14 command: AuditSubcommand,
15}
16
17#[derive(Debug, Subcommand)]
18enum AuditSubcommand {
19 #[command(visible_aliases = &["list", "ls"])]
21 Show {
22 #[arg(short = 'n', long, default_value = "50")]
24 limit: usize,
25
26 #[arg(short = 't', long)]
28 event_type: Option<String>,
29
30 #[arg(short = 's', long)]
32 severity: Option<String>,
33
34 #[arg(short = 'u', long)]
36 user: Option<String>,
37 },
38
39 #[command(visible_aliases = &["search", "find"])]
41 Query {
42 pattern: String,
44
45 #[arg(short = 'n', long, default_value = "100")]
47 limit: usize,
48
49 #[arg(long)]
51 failed_only: bool,
52 },
53
54 #[command(visible_aliases = &["statistics", "info"])]
56 Stats,
57
58 #[command(visible_aliases = &["clean", "purge"])]
60 Clear {
61 #[arg(short = 'y', long)]
63 yes: bool,
64 },
65
66 #[command(visible_aliases = &["save", "dump"])]
68 Export {
69 path: PathBuf,
71
72 #[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 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 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 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 if failed_only {
240 entries.retain(|entry| entry.exit_code.is_some() && entry.exit_code != Some(0));
241 }
242
243 entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
245
246 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
416pub async fn handle_audit_command(command: AuditCommand, format: OutputFormat) -> Result<()> {
418 command.execute(format).await
419}