tracexec_core/
cli.rs

1use std::{
2  io::{
3    BufWriter,
4    stderr,
5    stdout,
6  },
7  path::PathBuf,
8};
9
10use args::{
11  DebuggerArgs,
12  PtraceArgs,
13  TuiModeArgs,
14};
15use clap::{
16  CommandFactory,
17  Parser,
18  Subcommand,
19};
20use config::Config;
21use options::ExportFormat;
22use tracing::debug;
23
24use self::{
25  args::{
26    LogModeArgs,
27    ModifierArgs,
28    TracerEventArgs,
29  },
30  options::Color,
31};
32use crate::{
33  cli::args::ExporterArgs,
34  output::Output,
35};
36
37pub mod args;
38pub mod config;
39pub mod options;
40pub mod theme;
41
42#[derive(Parser, Debug)]
43#[clap(author, version, about)]
44pub struct Cli {
45  #[arg(long, default_value_t = Color::Auto, help = "Control whether colored output is enabled. This flag has no effect on TUI mode.")]
46  pub color: Color,
47  #[arg(
48    short = 'C',
49    long,
50    help = "Change current directory to this path before doing anything"
51  )]
52  pub cwd: Option<PathBuf>,
53  #[arg(
54    short = 'P',
55    long,
56    help = "Load profile from this path",
57    conflicts_with = "no_profile"
58  )]
59  pub profile: Option<PathBuf>,
60  #[arg(long, help = "Do not load profiles")]
61  pub no_profile: bool,
62  #[arg(
63    short,
64    long,
65    help = "Run as user. This option is only available when running tracexec as root"
66  )]
67  pub user: Option<String>,
68  #[clap(subcommand)]
69  pub cmd: CliCommand,
70}
71
72#[derive(Subcommand, Debug)]
73pub enum CliCommand {
74  #[clap(about = "Run tracexec in logging mode")]
75  Log {
76    #[arg(last = true, required = true, help = "command to be executed")]
77    cmd: Vec<String>,
78    #[clap(flatten)]
79    tracing_args: LogModeArgs,
80    #[clap(flatten)]
81    modifier_args: ModifierArgs,
82    #[clap(flatten)]
83    ptrace_args: PtraceArgs,
84    #[clap(flatten)]
85    tracer_event_args: TracerEventArgs,
86    #[clap(
87      short,
88      long,
89      help = "Output, stderr by default. A single hyphen '-' represents stdout."
90    )]
91    output: Option<PathBuf>,
92  },
93  #[clap(about = "Run tracexec in TUI mode, stdin/out/err are redirected to /dev/null by default")]
94  Tui {
95    #[arg(last = true, required = true, help = "command to be executed")]
96    cmd: Vec<String>,
97    #[clap(flatten)]
98    modifier_args: ModifierArgs,
99    #[clap(flatten)]
100    ptrace_args: PtraceArgs,
101    #[clap(flatten)]
102    tracer_event_args: TracerEventArgs,
103    #[clap(flatten)]
104    tui_args: TuiModeArgs,
105    #[clap(flatten)]
106    debugger_args: DebuggerArgs,
107  },
108  #[clap(about = "Generate shell completions for tracexec")]
109  GenerateCompletions {
110    #[arg(required = true, help = "The shell to generate completions for")]
111    shell: clap_complete::Shell,
112  },
113  #[clap(about = "Collect exec events and export them")]
114  Collect {
115    #[arg(last = true, required = true, help = "command to be executed")]
116    cmd: Vec<String>,
117    #[clap(flatten)]
118    modifier_args: ModifierArgs,
119    #[clap(flatten)]
120    ptrace_args: PtraceArgs,
121    #[clap(flatten)]
122    exporter_args: ExporterArgs,
123    #[clap(short = 'F', long, help = "the format for exported exec events")]
124    format: ExportFormat,
125    #[clap(
126      short,
127      long,
128      help = "Output, stderr by default. A single hyphen '-' represents stdout."
129    )]
130    output: Option<PathBuf>,
131    #[clap(
132      long,
133      help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
134      conflicts_with = "no_foreground"
135    )]
136    foreground: bool,
137    #[clap(
138      long,
139      help = "Do not set the terminal foreground process group to tracee",
140      conflicts_with = "foreground"
141    )]
142    no_foreground: bool,
143  },
144  #[cfg(feature = "ebpf")]
145  #[clap(about = "Experimental ebpf mode")]
146  Ebpf {
147    #[clap(subcommand)]
148    command: EbpfCommand,
149  },
150}
151
152#[derive(Subcommand, Debug)]
153#[cfg(feature = "ebpf")]
154pub enum EbpfCommand {
155  #[clap(about = "Run tracexec in logging mode")]
156  Log {
157    #[arg(
158      last = true,
159      help = "command to be executed. Leave it empty to trace all exec on system"
160    )]
161    cmd: Vec<String>,
162    #[clap(
163      short,
164      long,
165      help = "Output, stderr by default. A single hyphen '-' represents stdout."
166    )]
167    output: Option<PathBuf>,
168    #[clap(flatten)]
169    modifier_args: ModifierArgs,
170    #[clap(flatten)]
171    log_args: LogModeArgs,
172  },
173  #[clap(about = "Run tracexec in TUI mode, stdin/out/err are redirected to /dev/null by default")]
174  Tui {
175    #[arg(
176      last = true,
177      help = "command to be executed. Leave it empty to trace all exec on system"
178    )]
179    cmd: Vec<String>,
180    #[clap(flatten)]
181    modifier_args: ModifierArgs,
182    #[clap(flatten)]
183    tracer_event_args: TracerEventArgs,
184    #[clap(flatten)]
185    tui_args: TuiModeArgs,
186  },
187  #[clap(about = "Collect exec events and export them")]
188  Collect {
189    #[arg(
190      last = true,
191      help = "command to be executed. Leave it empty to trace all exec on system"
192    )]
193    cmd: Vec<String>,
194    #[clap(flatten)]
195    modifier_args: ModifierArgs,
196    #[clap(short = 'F', long, help = "the format for exported exec events")]
197    format: ExportFormat,
198    #[clap(flatten)]
199    exporter_args: ExporterArgs,
200    #[clap(
201      short,
202      long,
203      help = "Output, stderr by default. A single hyphen '-' represents stdout."
204    )]
205    output: Option<PathBuf>,
206    #[clap(
207      long,
208      help = "Set the terminal foreground process group to tracee. This option is useful when tracexec is used interactively. [default]",
209      conflicts_with = "no_foreground"
210    )]
211    foreground: bool,
212    #[clap(
213      long,
214      help = "Do not set the terminal foreground process group to tracee",
215      conflicts_with = "foreground"
216    )]
217    no_foreground: bool,
218  },
219}
220
221impl Cli {
222  pub fn get_output(path: Option<PathBuf>, color: Color) -> std::io::Result<Box<Output>> {
223    Ok(match path {
224      None => Box::new(stderr()),
225      Some(ref x) if x.as_os_str() == "-" => Box::new(stdout()),
226      Some(path) => {
227        let file = std::fs::OpenOptions::new()
228          .create(true)
229          .truncate(true)
230          .write(true)
231          .open(path)?;
232        if color != Color::Always {
233          // Disable color by default when output is file
234          owo_colors::control::set_should_colorize(false);
235        }
236        Box::new(BufWriter::new(file))
237      }
238    })
239  }
240
241  pub fn generate_completions(shell: clap_complete::Shell) {
242    let mut cmd = Self::command();
243    clap_complete::generate(shell, &mut cmd, env!("CARGO_CRATE_NAME"), &mut stdout())
244  }
245
246  pub fn merge_config(&mut self, config: Config) {
247    debug!("Merging config: {config:?}");
248    match &mut self.cmd {
249      CliCommand::Log {
250        tracing_args,
251        modifier_args,
252        ptrace_args,
253        ..
254      } => {
255        if let Some(c) = config.ptrace {
256          ptrace_args.merge_config(c);
257        }
258        if let Some(c) = config.modifier {
259          modifier_args.merge_config(c);
260        }
261        if let Some(c) = config.log {
262          tracing_args.merge_config(c);
263        }
264      }
265      CliCommand::Tui {
266        modifier_args,
267        ptrace_args,
268        tui_args,
269        debugger_args,
270        ..
271      } => {
272        if let Some(c) = config.ptrace {
273          ptrace_args.merge_config(c);
274        }
275        if let Some(c) = config.modifier {
276          modifier_args.merge_config(c);
277        }
278        if let Some(c) = config.tui {
279          tui_args.merge_config(c);
280        }
281        if let Some(c) = config.debugger {
282          debugger_args.merge_config(c);
283        }
284      }
285      CliCommand::Collect {
286        foreground,
287        no_foreground,
288        ptrace_args,
289        ..
290      } => {
291        if let Some(c) = config.ptrace {
292          ptrace_args.merge_config(c);
293        }
294        if let Some(c) = config.log
295          && (!*foreground)
296          && (!*no_foreground)
297          && let Some(x) = c.foreground
298        {
299          if x {
300            *foreground = true;
301          } else {
302            *no_foreground = true;
303          }
304        }
305      }
306      _ => (),
307    }
308  }
309}
310
311#[cfg(test)]
312mod tests {
313  use std::{
314    fs,
315    io::Write,
316    path::PathBuf,
317  };
318
319  use super::*;
320  use crate::cli::{
321    args::{
322      DebuggerArgs,
323      LogModeArgs,
324      ModifierArgs,
325      PtraceArgs,
326      TracerEventArgs,
327      TuiModeArgs,
328    },
329    config::{
330      Config,
331      DebuggerConfig,
332      LogModeConfig,
333      ModifierConfig,
334      PtraceConfig,
335      TuiModeConfig,
336    },
337    options::{
338      Color,
339      ExportFormat,
340      SeccompBpf,
341    },
342  };
343
344  #[test]
345  fn test_cli_parse_log() {
346    let args = vec![
347      "tracexec",
348      "log",
349      "--show-interpreter",
350      "--successful-only",
351      "--",
352      "echo",
353      "hello",
354    ];
355    let cli = Cli::parse_from(args);
356
357    if let CliCommand::Log {
358      cmd,
359      tracing_args,
360      modifier_args,
361      ..
362    } = cli.cmd
363    {
364      assert_eq!(cmd, vec!["echo", "hello"]);
365      assert!(tracing_args.show_interpreter);
366      assert!(modifier_args.successful_only);
367    } else {
368      panic!("Expected Log command");
369    }
370  }
371
372  #[test]
373  fn test_cli_parse_tui() {
374    let args = vec!["tracexec", "tui", "--tty", "--follow", "--", "bash"];
375    let cli = Cli::parse_from(args);
376
377    if let CliCommand::Tui { cmd, tui_args, .. } = cli.cmd {
378      assert_eq!(cmd, vec!["bash"]);
379      assert!(tui_args.tty);
380      assert!(tui_args.follow);
381    } else {
382      panic!("Expected Tui command");
383    }
384  }
385
386  #[test]
387  fn test_get_output_stderr_stdout_file() {
388    // default (None) -> stderr
389    let out = Cli::get_output(None, Color::Auto).unwrap();
390    let _ = out; // just ensure it returns something
391
392    // "-" -> stdout
393    let out = Cli::get_output(Some(PathBuf::from("-")), Color::Auto).unwrap();
394    let _ = out;
395
396    // real file
397    let path = PathBuf::from("test_output.txt");
398    let mut out = Cli::get_output(Some(path.clone()), Color::Auto).unwrap();
399    writeln!(out, "Hello world").unwrap();
400    drop(out);
401
402    let content = fs::read_to_string(path.clone()).unwrap();
403    assert!(content.contains("Hello world"));
404    fs::remove_file(path).unwrap();
405  }
406
407  #[test]
408  fn test_merge_config_log() {
409    let mut cli = Cli {
410      color: Color::Auto,
411      cwd: None,
412      profile: None,
413      no_profile: false,
414      user: None,
415      cmd: CliCommand::Log {
416        cmd: vec!["ls".into()],
417        tracing_args: LogModeArgs {
418          show_interpreter: false,
419          ..Default::default()
420        },
421        modifier_args: ModifierArgs::default(),
422        ptrace_args: PtraceArgs::default(),
423        tracer_event_args: TracerEventArgs::all(),
424        output: None,
425      },
426    };
427
428    let config = Config {
429      ptrace: Some(PtraceConfig {
430        seccomp_bpf: Some(SeccompBpf::On),
431      }),
432      modifier: Some(ModifierConfig {
433        successful_only: Some(true),
434        ..Default::default()
435      }),
436      log: Some(LogModeConfig {
437        show_interpreter: Some(true),
438        ..Default::default()
439      }),
440      tui: None,
441      debugger: None,
442    };
443
444    cli.merge_config(config);
445
446    if let CliCommand::Log {
447      tracing_args,
448      modifier_args,
449      ptrace_args,
450      ..
451    } = cli.cmd
452    {
453      assert!(tracing_args.show_interpreter);
454      assert!(modifier_args.successful_only);
455      assert_eq!(ptrace_args.seccomp_bpf, SeccompBpf::On);
456    } else {
457      panic!("Expected Log command");
458    }
459  }
460
461  #[test]
462  fn test_merge_config_tui() {
463    let mut cli = Cli {
464      color: Color::Auto,
465      cwd: None,
466      profile: None,
467      no_profile: false,
468      user: None,
469      cmd: CliCommand::Tui {
470        cmd: vec!["bash".into()],
471        modifier_args: ModifierArgs::default(),
472        ptrace_args: PtraceArgs::default(),
473        tracer_event_args: TracerEventArgs::all(),
474        tui_args: TuiModeArgs::default(),
475        debugger_args: DebuggerArgs::default(),
476      },
477    };
478
479    let config = Config {
480      ptrace: Some(PtraceConfig {
481        seccomp_bpf: Some(SeccompBpf::Off),
482      }),
483      modifier: Some(ModifierConfig {
484        successful_only: Some(true),
485        ..Default::default()
486      }),
487      log: None,
488      tui: Some(TuiModeConfig {
489        follow: Some(true),
490        frame_rate: Some(30.0),
491        ..Default::default()
492      }),
493      debugger: Some(DebuggerConfig {
494        default_external_command: Some("echo hello".into()),
495      }),
496    };
497
498    cli.merge_config(config);
499
500    if let CliCommand::Tui {
501      modifier_args,
502      ptrace_args,
503      tui_args,
504      debugger_args,
505      ..
506    } = cli.cmd
507    {
508      assert!(modifier_args.successful_only);
509      assert_eq!(ptrace_args.seccomp_bpf, SeccompBpf::Off);
510      assert_eq!(tui_args.frame_rate.unwrap(), 30.0);
511      assert_eq!(
512        debugger_args.default_external_command.as_ref().unwrap(),
513        "echo hello"
514      );
515    } else {
516      panic!("Expected Tui command");
517    }
518  }
519
520  #[test]
521  fn test_merge_config_collect_foreground() {
522    let mut cli = Cli {
523      color: Color::Auto,
524      cwd: None,
525      profile: None,
526      no_profile: false,
527      user: None,
528      cmd: CliCommand::Collect {
529        cmd: vec!["ls".into()],
530        modifier_args: ModifierArgs::default(),
531        ptrace_args: PtraceArgs::default(),
532        exporter_args: Default::default(),
533        format: ExportFormat::Json,
534        output: None,
535        foreground: false,
536        no_foreground: false,
537      },
538    };
539
540    let config = Config {
541      log: Some(LogModeConfig {
542        foreground: Some(true),
543        ..Default::default()
544      }),
545      ptrace: None,
546      modifier: None,
547      tui: None,
548      debugger: None,
549    };
550
551    cli.merge_config(config);
552
553    if let CliCommand::Collect {
554      foreground,
555      no_foreground,
556      ..
557    } = cli.cmd
558    {
559      assert!(foreground);
560      assert!(!no_foreground);
561    } else {
562      panic!("Expected Collect command");
563    }
564  }
565
566  #[test]
567  fn test_generate_completions_smoke() {
568    // smoke test: just run without panicking
569    Cli::generate_completions(clap_complete::Shell::Bash);
570  }
571}