1use std::{
2 borrow::Cow,
3 ops::Range,
4};
5
6use itertools::Itertools;
7use nix::fcntl::OFlag;
8
9use crate::{
10 cli::args::ModifierArgs,
11 event::{
12 ExecEvent,
13 OutputMsg,
14 RuntimeModifier,
15 TracerEventDetails,
16 },
17 proc::{
18 BaselineInfo,
19 FileDescriptorInfoCollection,
20 },
21};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CopyTarget {
25 Line,
26 Commandline(SupportedShell),
27 CommandlineWithFullEnv(SupportedShell),
28 CommandlineWithStdio(SupportedShell),
29 CommandlineWithFds(SupportedShell),
30 Env,
31 Argv,
32 ArgvJoined,
33 Filename,
34 SyscallResult,
35 EnvDiff,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SupportedShell {
40 Bash,
41 Sh,
42 Fish,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CommandlinePartKind {
47 Plain,
48 TracerEvent,
49 Arg0,
50 Cwd,
51 DeletedEnvVar,
52 AddedEnvVar,
53 ModifiedEnvVar,
54 UnchangedEnvKey,
55 UnchangedEnvVal,
56 Filename,
57 Argv,
58 InlineTracerError,
59 ModifiedFdInCommandline,
60 RemovedFdInCommandline,
61 CloexecFdInCommandline,
62 AddedFdInCommandline,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum OutputStatus {
67 Ok,
68 PartialOk,
69 Err,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct CommandlinePart {
74 pub text: String,
75 pub kind: CommandlinePartKind,
76 pub output_status: Option<OutputStatus>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Commandline {
81 pub parts: Vec<CommandlinePart>,
82 pub cwd_range: Option<Range<usize>>,
83 pub env_range: Option<Range<usize>>,
84}
85
86impl std::fmt::Display for Commandline {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 for part in &self.parts {
89 f.write_str(&part.text)?;
90 }
91 Ok(())
92 }
93}
94
95impl Commandline {
96 fn push(&mut self, text: impl Into<String>, kind: CommandlinePartKind) {
97 self.parts.push(CommandlinePart {
98 text: text.into(),
99 kind,
100 output_status: None,
101 });
102 }
103
104 fn push_bash_escaped(&mut self, msg: &OutputMsg, kind: CommandlinePartKind) {
105 self.parts.push(CommandlinePart {
106 text: msg.bash_escaped().into_owned(),
107 kind,
108 output_status: Some(output_status(msg)),
109 });
110 }
111}
112
113fn output_status(msg: &OutputMsg) -> OutputStatus {
114 match msg {
115 OutputMsg::Ok(_) => OutputStatus::Ok,
116 OutputMsg::PartialOk(_) => OutputStatus::PartialOk,
117 OutputMsg::Err(_) => OutputStatus::Err,
118 }
119}
120
121fn handle_stdio_fd(
122 fd: i32,
123 fdinfo_orig: &crate::proc::FileDescriptorInfo,
124 curr: &FileDescriptorInfoCollection,
125 commandline: &mut Commandline,
126) {
127 let (fdstr, redir) = match fd {
128 0 => (" 0", "<"),
129 1 => (" 1", ">"),
130 2 => (" 2", "2>"),
131 _ => unreachable!(),
132 };
133
134 if let Some(fdinfo) = curr.get(fd) {
135 if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
136 commandline.push(fdstr, CommandlinePartKind::CloexecFdInCommandline);
137 commandline.push(">&-", CommandlinePartKind::CloexecFdInCommandline);
138 } else if fdinfo.not_same_file_as(fdinfo_orig) {
139 commandline.push(" ", CommandlinePartKind::Plain);
140 commandline.push(redir, CommandlinePartKind::ModifiedFdInCommandline);
141 commandline.push_bash_escaped(&fdinfo.path, CommandlinePartKind::ModifiedFdInCommandline);
142 }
143 } else if curr.is_reliable() {
144 commandline.push(fdstr, CommandlinePartKind::CloexecFdInCommandline);
145 commandline.push(">&-", CommandlinePartKind::RemovedFdInCommandline);
146 }
147}
148
149pub fn exec_commandline(
150 event: &ExecEvent,
151 baseline: &BaselineInfo,
152 modifier: &ModifierArgs,
153 rt_modifier: RuntimeModifier,
154 full_env: bool,
155 _shell: SupportedShell,
156) -> Commandline {
157 let mut commandline = Commandline {
158 parts: Vec::new(),
159 cwd_range: None,
160 env_range: None,
161 };
162 commandline.push("env", CommandlinePartKind::TracerEvent);
163
164 let ExecEvent {
165 cwd,
166 filename,
167 argv,
168 env_diff,
169 fdinfo,
170 envp,
171 has_dash_env,
172 ..
173 } = event;
174
175 let _ = argv.as_deref().inspect(|v| {
177 v.first().inspect(|&arg0| {
178 if filename != arg0 {
179 commandline.push(" ", CommandlinePartKind::Plain);
180 commandline.push("-a ", CommandlinePartKind::Arg0);
181 commandline.push_bash_escaped(arg0, CommandlinePartKind::Arg0);
182 }
183 });
184 });
185
186 if cwd != &baseline.cwd && rt_modifier.show_cwd {
188 let start = commandline.parts.len();
189 commandline.push(" ", CommandlinePartKind::Plain);
190 commandline.push("-C ", CommandlinePartKind::Cwd);
191 commandline.push_bash_escaped(cwd, CommandlinePartKind::Cwd);
192 commandline.cwd_range = Some(start..commandline.parts.len());
193 }
194
195 if rt_modifier.show_env {
197 let start = commandline.parts.len();
198 if !full_env {
199 if let Ok(env_diff) = env_diff {
200 for k in env_diff.removed.iter() {
201 commandline.push(" ", CommandlinePartKind::Plain);
202 commandline.push("-u ", CommandlinePartKind::DeletedEnvVar);
203 commandline.push_bash_escaped(k, CommandlinePartKind::DeletedEnvVar);
204 }
205 if env_diff.need_env_argument_separator(filename) {
206 commandline.push(" ", CommandlinePartKind::Plain);
207 commandline.push("--", CommandlinePartKind::Plain);
208 }
209 for (k, v) in env_diff.added.iter() {
210 commandline.push(" ", CommandlinePartKind::Plain);
211 commandline.push_bash_escaped(k, CommandlinePartKind::AddedEnvVar);
212 commandline.push("=", CommandlinePartKind::AddedEnvVar);
213 commandline.push_bash_escaped(v, CommandlinePartKind::AddedEnvVar);
214 }
215 for (k, v) in env_diff.modified.iter() {
216 commandline.push(" ", CommandlinePartKind::Plain);
217 commandline.push_bash_escaped(k, CommandlinePartKind::ModifiedEnvVar);
218 commandline.push("=", CommandlinePartKind::ModifiedEnvVar);
219 commandline.push_bash_escaped(v, CommandlinePartKind::ModifiedEnvVar);
220 }
221 }
222 } else if let Ok(envp) = &**envp {
223 commandline.push(" ", CommandlinePartKind::Plain);
224 commandline.push("-i", CommandlinePartKind::Plain);
225 if *has_dash_env || (envp.is_empty() && filename.as_ref().starts_with('-')) {
226 commandline.push(" --", CommandlinePartKind::Plain);
227 }
228 for (k, v) in envp.iter() {
229 commandline.push(" ", CommandlinePartKind::Plain);
230 commandline.push_bash_escaped(k, CommandlinePartKind::UnchangedEnvKey);
231 commandline.push("=", CommandlinePartKind::UnchangedEnvKey);
232 commandline.push_bash_escaped(v, CommandlinePartKind::UnchangedEnvVal);
233 }
234 }
235 commandline.env_range = Some(start..commandline.parts.len());
236 }
237
238 commandline.push(" ", CommandlinePartKind::Plain);
239 commandline.push_bash_escaped(filename, CommandlinePartKind::Filename);
241
242 match argv.as_ref() {
244 Ok(argv) => {
245 for arg in argv.iter().skip(1) {
246 commandline.push(" ", CommandlinePartKind::Plain);
247 commandline.push_bash_escaped(arg, CommandlinePartKind::Argv);
248 }
249 }
250 Err(_) => {
251 commandline.push(" ", CommandlinePartKind::Plain);
252 commandline.push(
253 "[failed to read argv]",
254 CommandlinePartKind::InlineTracerError,
255 );
256 }
257 }
258
259 if modifier.stdio_in_cmdline {
261 for (fd, fdinfo_orig) in baseline.fdinfo.stdio() {
262 handle_stdio_fd(fd, fdinfo_orig, fdinfo, &mut commandline);
263 }
264 }
265
266 if modifier.fd_in_cmdline {
267 for (&fd, fdinfo) in fdinfo.fdinfo.iter() {
268 if fd < 3 {
269 continue;
270 }
271 if fdinfo.flags.ok().is_none() || fdinfo.flags.intersects(OFlag::O_CLOEXEC) {
272 continue;
274 }
275 commandline.push(" ", CommandlinePartKind::Plain);
276 commandline.push(fd.to_string(), CommandlinePartKind::AddedFdInCommandline);
277 commandline.push("<>", CommandlinePartKind::AddedFdInCommandline);
278 commandline.push_bash_escaped(&fdinfo.path, CommandlinePartKind::AddedFdInCommandline);
279 }
280 }
281
282 commandline
283}
284
285pub fn text_for_copy<'a>(
286 event_details: &'a TracerEventDetails,
287 baseline: &BaselineInfo,
288 target: CopyTarget,
289 _modifier_args: &ModifierArgs,
290 _rt_modifier: RuntimeModifier,
291) -> Cow<'a, str> {
292 let TracerEventDetails::Exec(event) = event_details else {
293 panic!("Copy target {target:?} is only available for Exec events");
294 };
295 let mut modifier_args = ModifierArgs::default();
296 match target {
297 CopyTarget::Commandline(shell) => exec_commandline(
298 event,
299 baseline,
300 &modifier_args,
301 RuntimeModifier::default(),
302 false,
303 shell,
304 )
305 .to_string()
306 .into(),
307 CopyTarget::CommandlineWithFullEnv(shell) => exec_commandline(
308 event,
309 baseline,
310 &modifier_args,
311 RuntimeModifier::default(),
312 true,
313 shell,
314 )
315 .to_string()
316 .into(),
317 CopyTarget::CommandlineWithStdio(shell) => {
318 modifier_args.stdio_in_cmdline = true;
319 exec_commandline(
320 event,
321 baseline,
322 &modifier_args,
323 RuntimeModifier::default(),
324 false,
325 shell,
326 )
327 .to_string()
328 .into()
329 }
330 CopyTarget::CommandlineWithFds(shell) => {
331 modifier_args.fd_in_cmdline = true;
332 modifier_args.stdio_in_cmdline = true;
333 exec_commandline(
334 event,
335 baseline,
336 &modifier_args,
337 RuntimeModifier::default(),
338 false,
339 shell,
340 )
341 .to_string()
342 .into()
343 }
344 CopyTarget::Env => match event.envp.as_ref() {
345 Ok(envp) => envp
346 .iter()
347 .map(|(k, v)| format!("{k}={v}"))
348 .join("\n")
349 .into(),
350 Err(e) => format!("[failed to read envp: {e}]").into(),
351 },
352 CopyTarget::EnvDiff => {
353 let Ok(env_diff) = event.env_diff.as_ref() else {
354 return "[failed to read envp]".into();
355 };
356 let mut result = String::new();
357 result.push_str("# Added:\n");
358 for (k, v) in env_diff.added.iter() {
359 result.push_str(&format!("{k}={v}\n"));
360 }
361 result.push_str("# Modified: (original first)\n");
362 for (k, original, current) in env_diff.modified_with_values(&baseline.env) {
363 result.push_str(&format!("{}={}\n{}={}\n", k, original, k, current));
364 }
365 result.push_str("# Removed:\n");
366 for (k, value) in env_diff.removed_with_values(&baseline.env) {
367 result.push_str(&format!("{k}={value}\n"));
368 }
369 result.into()
370 }
371 CopyTarget::Argv => TracerEventDetails::argv_to_string(&event.argv).into(),
372 CopyTarget::ArgvJoined => match event.argv.as_ref() {
373 Ok(argv) => argv.iter().map(AsRef::as_ref).join(" ").into(),
374 Err(_) => "[failed to read argv]".into(),
375 },
376 CopyTarget::Filename => Cow::Borrowed(event.filename.as_ref()),
377 CopyTarget::SyscallResult => event.result.to_string().into(),
378 CopyTarget::Line => panic!("CopyTarget::Line requires a presentation formatter"),
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use std::{
385 collections::BTreeMap,
386 sync::Arc,
387 };
388
389 use nix::unistd::Pid;
390
391 use super::*;
392 use crate::{
393 event::ExecSyscall,
394 proc::{
395 CgroupInfo,
396 Cred,
397 diff_env,
398 },
399 };
400
401 fn msg(value: &str) -> OutputMsg {
402 OutputMsg::Ok(value.into())
403 }
404
405 fn filename_event(
406 baseline: &BaselineInfo,
407 envp: BTreeMap<OutputMsg, OutputMsg>,
408 filename: &str,
409 ) -> ExecEvent {
410 let has_dash_env = envp.keys().any(|key| key.as_ref().starts_with('-'));
411 let filename = msg(filename);
412 ExecEvent {
413 syscall: ExecSyscall::Execve,
414 exec_pid: Pid::from_raw(1),
415 pid: Pid::from_raw(1),
416 cwd: baseline.cwd.clone(),
417 comm: "false".into(),
418 filename: filename.clone(),
419 argv: Arc::new(Ok(vec![filename])),
420 env_diff: Ok(diff_env(&baseline.env, &envp)),
421 envp: Arc::new(Ok(envp)),
422 has_dash_env,
423 cred: Ok(Cred::default()),
424 interpreter: None,
425 fdinfo: Arc::new(FileDescriptorInfoCollection::default()),
426 result: 0,
427 timestamp: chrono::Local::now(),
428 parent: None,
429 cgroup: CgroupInfo::NotCollected,
430 }
431 }
432
433 fn baseline(env: BTreeMap<OutputMsg, OutputMsg>) -> BaselineInfo {
434 BaselineInfo {
435 cwd: msg("/work"),
436 env,
437 fdinfo: FileDescriptorInfoCollection::default(),
438 }
439 }
440
441 #[test]
442 fn diff_env_commandline_separates_dash_filename_when_env_is_unchanged() {
443 let env = BTreeMap::from([(msg("UNCHANGED"), msg("value"))]);
444 let baseline = baseline(env.clone());
445 let event = filename_event(&baseline, env, "--ignore-signal");
446
447 let commandline = exec_commandline(
448 &event,
449 &baseline,
450 &ModifierArgs::default(),
451 RuntimeModifier::default(),
452 false,
453 SupportedShell::Bash,
454 );
455
456 assert_eq!(commandline.to_string(), "env -- --ignore-signal");
457 }
458
459 #[test]
460 fn full_env_commandline_separates_dash_filename_when_env_is_empty() {
461 let baseline = baseline(BTreeMap::new());
462 let event = filename_event(&baseline, BTreeMap::new(), "--ignore-signal");
463
464 let commandline = exec_commandline(
465 &event,
466 &baseline,
467 &ModifierArgs::default(),
468 RuntimeModifier::default(),
469 true,
470 SupportedShell::Bash,
471 );
472
473 assert_eq!(commandline.to_string(), "env -i -- --ignore-signal");
474 }
475
476 #[test]
477 fn empty_env_commandlines_do_not_separate_non_dash_filename() {
478 let baseline = baseline(BTreeMap::new());
479 let event = filename_event(&baseline, BTreeMap::new(), "/bin/false");
480
481 let diff_env = exec_commandline(
482 &event,
483 &baseline,
484 &ModifierArgs::default(),
485 RuntimeModifier::default(),
486 false,
487 SupportedShell::Bash,
488 );
489 let full_env = exec_commandline(
490 &event,
491 &baseline,
492 &ModifierArgs::default(),
493 RuntimeModifier::default(),
494 true,
495 SupportedShell::Bash,
496 );
497
498 assert_eq!(diff_env.to_string(), "env /bin/false");
499 assert_eq!(full_env.to_string(), "env -i /bin/false");
500 }
501}