mod analysis;
mod cli;
mod fs;
mod macros;
mod parse;
use std::collections::HashSet;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::{env, process};
use anyhow::Result;
use clap::crate_name;
use flexi_logger::{opt_format, Logger};
use termcolor::{Color, ColorChoice, WriteColor};
use which::which;
use parse::{string::decode_hex, StraceLine, StraceToken};
fn init_logging() -> Result<PathBuf> {
let log_dir = env::temp_dir().join(format!(".{}", crate_name!()));
Logger::with_env()
.log_to_file()
.directory(&log_dir)
.format(opt_format)
.start()?;
log::trace!("--- LOGGER INITIALISED ---");
Ok(log_dir)
}
fn main() -> Result<()> {
let log_dir = match init_logging() {
Ok(dir) => dir,
Err(e) => {
eprintln!("Failed to initialise logger: {}", e);
process::exit(1);
}
};
macro_rules! exit_with_error {
($( $eprintln_arg:expr ),*) => {{
log::error!($( $eprintln_arg ),*);
p!(false, None, $( $eprintln_arg ),*);
p!(false, None, "Logs available at: {}", log_dir.display());
process::exit(1);
}};
}
let strace_path = match which("strace") {
Ok(path) => path,
Err(e) => exit_with_error!("Failed to find `strace` binary: {}", e),
};
let app_args = cli::Args::parse();
log::trace!("{:?}", app_args);
if app_args.pid.is_none() && app_args.cmd.is_empty() {
use clap::IntoApp;
cli::Args::into_app().print_help().unwrap();
exit_with_error!("No command or pid given!");
}
let mut child = Command::new(strace_path)
.arg("--follow-forks")
.arg("--status=successful,failed,unfinished,unavailable,detached")
.arg("-ttt")
.arg("--strings-in-hex")
.arg("--trace=%file")
.arg("--no-abbrev")
.args(&app_args.cmd)
.args(
&app_args
.pid
.map(|pid| vec![format!("--attach={}", pid)])
.unwrap_or(vec![]),
)
.stderr(Stdio::piped())
.stdout(Stdio::null())
.spawn()?;
let reader = BufReader::new(child.stderr.as_mut().unwrap());
let seen_values = Arc::new(Mutex::new(HashSet::new()));
for line in reader.lines() {
let line = line?;
log::trace!("RAW LINE: {}", line);
match StraceLine::from_str(&line) {
Ok(strace) => {
log::debug!("PARSED LINE: {}", strace);
if let StraceToken::PermissionDenied(pid) = strace.inner {
p!(
app_args.color,
Color::Yellow,
"{}\n{}",
format!("Could not attach to pid: {}, permission denied.", pid),
"Try re-running the command with elevated permissons."
);
break;
}
let app_args = &app_args;
let file_types = app_args.file_types();
let seen_values = &seen_values;
strace.walk(&move |token| {
if let StraceToken::Call {
name, result, args, ..
} = token
{
let result = match result {
Some(result) => result,
None => return true,
};
let fn_info = &analysis::FN_MAP[name];
let color = match fn_info.did_succeed(*result) {
Some(true) => Color::Green,
Some(false) => {
if app_args.non_existent {
Color::Yellow
} else {
return true;
}
}
None => Color::White,
};
let maybe_paths = if *name == "execve" {
if let StraceToken::String(s) = &args[0] {
vec![*s]
} else {
vec![]
}
} else {
token.strs()
};
for s in maybe_paths {
let s = decode_hex(s);
if let Some(file_types) = file_types {
let path = Path::new(&s);
match path.metadata() {
Ok(meta) => {
let ft = meta.file_type();
if (file_types.files && !fs::is_file(&path))
|| (file_types.directories && !fs::is_dir(&path))
|| (file_types.symlinks && !fs::is_symlink(&path))
|| (file_types.sockets && !fs::is_socket(&ft))
|| (file_types.pipes && !fs::is_pipe(&ft))
|| (file_types.executables && !fs::is_executable(&meta))
|| (file_types.empty && !fs::is_empty(&path))
{
continue;
}
}
Err(_) => continue,
}
}
if s.is_empty() {
continue;
}
if app_args.no_duplicates {
if seen_values.lock().unwrap().contains(&s) {
continue;
}
seen_values.lock().unwrap().insert(s.clone());
}
p!(app_args.color, color, "{:?}", s);
}
false
} else {
true
}
});
}
#[allow(unused)]
Err(e) => {
log::warn!("INVALID LINE: {}", line);
if app_args.invalid_lines {
#[cfg(not(debug_assertions))]
p!(app_args.color, Color::Red, "PARSE_ERR: {}", line);
#[cfg(debug_assertions)]
p!(app_args.color, Color::Red, "{}", e);
}
}
}
}
p!(app_args.color, None);
match child.wait() {
Ok(exit_status) => {
let msg = format!(
"strace exited with code: {}",
exit_status
.code()
.map(|c| c.to_string())
.unwrap_or("???".to_string())
);
if !exit_status.success() {
exit_with_error!("{}", msg);
} else {
log::trace!("{}", msg);
}
}
Err(e) => exit_with_error!("An error occurred while waiting for process to end: {}", e),
}
Ok(())
}