Skip to main content

lurk_cli/
args.rs

1use crate::arch::{
2    TRACE_CLOCK, TRACE_CREDS, TRACE_DESC, TRACE_FILE, TRACE_FSTAT, TRACE_FSTATFS, TRACE_IPC,
3    TRACE_LSTAT, TRACE_MEMORY, TRACE_NETWORK, TRACE_PROCESS, TRACE_PURE, TRACE_SIGNAL, TRACE_STAT,
4    TRACE_STATFS, TRACE_STATFS_LIKE, TRACE_STAT_LIKE,
5};
6use crate::syscall_info::RetCode;
7use anyhow::bail;
8use clap::{Parser, Subcommand};
9use libc::pid_t;
10use regex::Regex;
11use std::collections::HashMap;
12use std::path::PathBuf;
13use std::str::FromStr;
14use syscalls::{Sysno, SysnoSet};
15
16#[derive(Parser, Debug, Default)]
17#[command(name = "lurk", about, version, allow_external_subcommands = true)]
18pub struct Args {
19    /// Display system call numbers
20    #[arg(short = 'n', long)]
21    pub syscall_number: bool,
22    /// Attach to a running process
23    #[arg(short = 'p', long)]
24    pub attach: Option<pid_t>,
25    /// Print un-abbreviated versions of strings
26    #[arg(short = 'v', long)]
27    pub no_abbrev: bool,
28    /// Maximum string argument size to print
29    #[arg(short, long, conflicts_with = "no_abbrev")]
30    pub string_limit: Option<usize>,
31    /// Name of the file to print output to
32    #[arg(short = 'o', long)]
33    pub file: Option<PathBuf>,
34    /// Report a summary instead of the regular output
35    #[arg(short = 'c', long)]
36    pub summary_only: bool,
37    /// Report a summary in addition to the regular output
38    #[arg(short = 'C', long, conflicts_with = "summary_only")]
39    pub summary: bool,
40    /// Print only syscalls that returned without an error code
41    #[arg(short = 'z', long)]
42    pub successful_only: bool,
43    /// Print only syscalls that returned with an error code
44    #[arg(short = 'Z', long, conflicts_with = "successful_only")]
45    pub failed_only: bool,
46    /// --env var=val adds an environment variable. --env var removes an environment variable.
47    #[arg(short = 'E', long)]
48    pub env: Vec<String>,
49    /// Run the command with uid, gid and supplementary groups of username.
50    #[arg(short, long)]
51    pub username: Option<String>,
52    /// Trace child processes as they are created by currently traced processes.
53    #[arg(short, long)]
54    pub follow_forks: bool,
55    /// Show the time spent in system calls in ms.
56    #[arg(short = 'T', long)]
57    pub syscall_times: bool,
58    /// A qualifying expression which modifies which events to trace or how to trace them.
59    #[arg(short, long)]
60    pub expr: Vec<String>,
61    /// Display output in JSON format
62    #[arg(short, long)]
63    pub json: bool,
64    /// Collapse repeated failing `execve` attempts and only show final success
65    #[arg(long)]
66    pub collapse_exec_retries: bool,
67    #[command(subcommand)]
68    pub command: Option<ArgCommand>,
69}
70
71// The command/subcommand is a bit hacky, but gets the job done:
72// https://github.com/clap-rs/clap/discussions/4560#discussioncomment-5392780
73
74#[derive(Subcommand, Debug, PartialEq)]
75pub enum ArgCommand {
76    /// Trace command
77    #[command(external_subcommand)]
78    Command(Vec<String>),
79}
80
81#[derive(Parser, Debug, PartialEq)]
82pub struct ArgAttach {
83    /// Attach to a running process with the given pid.
84    #[arg(short = 'p', long)]
85    pub attach: pid_t,
86}
87
88impl Args {
89    pub fn create_filter(&self) -> anyhow::Result<Filter> {
90        let all_syscall_names: HashMap<&'static str, Sysno> =
91            SysnoSet::all().iter().map(|v| (v.name(), v)).collect();
92        let mut expr_negation = false;
93        let mut system_calls = SysnoSet::empty();
94
95        // Sort system calls listed with --expr into their category to handle them accordingly
96        for token in &self.expr {
97            let mut tokens = token.splitn(2, '=');
98            match (tokens.next(), tokens.next()) {
99                (Some(token_key), Some(mut token_value))
100                    if token_key == "t" || token_key == "trace" =>
101                {
102                    if let Some(v) = token_value.strip_prefix('!') {
103                        token_value = v;
104                        expr_negation = true;
105                    }
106
107                    for part in token_value.split(',') {
108                        if let Some(part) = part.strip_prefix('/') {
109                            // The '/' prefix followed by a regex pattern to match system calls
110                            if let Ok(pattern) = Regex::new(part) {
111                                for (syscall, sysno) in &all_syscall_names {
112                                    if pattern.is_match(syscall) {
113                                        system_calls.insert(*sysno);
114                                    }
115                                }
116                            } else {
117                                bail!("Invalid regex pattern: {part}");
118                            }
119                        } else if let Some(part) = part.strip_prefix('%') {
120                            // The '%' prefix followed by the name of a syscalls category to trace
121                            system_calls = system_calls.union(match part {
122                                "file" => &TRACE_FILE,
123                                "process" => &TRACE_PROCESS,
124                                "network" | "net" => &TRACE_NETWORK,
125                                "signal" => &TRACE_SIGNAL,
126                                "ipc" => &TRACE_IPC,
127                                "desc" => &TRACE_DESC,
128                                "memory" => &TRACE_MEMORY,
129                                "creds" => &TRACE_CREDS,
130                                "stat" => &TRACE_STAT,
131                                "lstat" => &TRACE_LSTAT,
132                                "fstat" => &TRACE_FSTAT,
133                                "%stat" => &TRACE_STAT_LIKE,
134                                "statfs" => &TRACE_STATFS,
135                                "fstatfs" => &TRACE_FSTATFS,
136                                "%statfs" => &TRACE_STATFS_LIKE,
137                                "clock" => &TRACE_CLOCK,
138                                "pure" => &TRACE_PURE,
139                                v => bail!("Category '{v}' is not valid!"),
140                            });
141                        } else {
142                            // The optional '?' prefix will ignore unknown system calls
143                            let mut ignore_unknown = false;
144                            if let Some(v) = token_value.strip_prefix('?') {
145                                token_value = v;
146                                ignore_unknown = true;
147                            }
148                            if let Ok(val) = Sysno::from_str(part) {
149                                system_calls.insert(val);
150                            } else if !ignore_unknown {
151                                bail!("System call '{part}' is not valid!");
152                            }
153                        }
154                    }
155                }
156                _ => bail!("expr {token} is not supported. Please have a look at the syntax."),
157            }
158        }
159        Ok(Filter {
160            ret_code_filter: if self.successful_only {
161                FilterRetCode::Oks
162            } else if self.failed_only {
163                FilterRetCode::Errs
164            } else {
165                FilterRetCode::All
166            },
167            sysno_filter: if system_calls.count() == 0 {
168                FilterSysno::All
169            } else if expr_negation {
170                FilterSysno::Except(system_calls)
171            } else {
172                FilterSysno::Only(system_calls)
173            },
174        })
175    }
176}
177
178enum FilterRetCode {
179    All,
180    Oks,
181    Errs,
182}
183
184enum FilterSysno {
185    All,
186    Only(SysnoSet),
187    Except(SysnoSet),
188}
189
190pub struct Filter {
191    ret_code_filter: FilterRetCode,
192    sysno_filter: FilterSysno,
193}
194
195impl Filter {
196    pub fn matches(&mut self, sys_no: Sysno, res: RetCode) -> bool {
197        (
198            // Should this result code be printed?
199            match self.ret_code_filter {
200                FilterRetCode::All => true,
201                FilterRetCode::Oks => matches!(res, RetCode::Ok(_) | RetCode::Address(_)),
202                FilterRetCode::Errs => matches!(res, RetCode::Err(_)),
203            }
204        ) && (
205            // Should this sys_no be printed?
206            match &self.sysno_filter {
207                FilterSysno::All => true,
208                FilterSysno::Only(sysno_set) => sysno_set.contains(sys_no),
209                FilterSysno::Except(sysno_set) => !sysno_set.contains(sys_no),
210            }
211        )
212    }
213
214    pub fn all_enabled(&self) -> SysnoSet {
215        match &self.sysno_filter {
216            FilterSysno::All => SysnoSet::all(),
217            FilterSysno::Only(sysno_set) => sysno_set.clone(),
218            FilterSysno::Except(sysno_set) => SysnoSet::all().difference(sysno_set),
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_args_simple() {
229        let args = Args::parse_from(["lurk", "app"]);
230        assert_eq!(
231            args.command,
232            Some(ArgCommand::Command(vec!["app".to_string()])),
233        );
234    }
235}