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 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 let out = Cli::get_output(None, Color::Auto).unwrap();
390 let _ = out; let out = Cli::get_output(Some(PathBuf::from("-")), Color::Auto).unwrap();
394 let _ = out;
395
396 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 Cli::generate_completions(clap_complete::Shell::Bash);
570 }
571}