From 8036bbf76b687141c1b13f33274e8eaa88c341b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= Date: Thu, 27 Oct 2022 19:48:40 +0200 Subject: [PATCH] Rework command line interface Use subcommand instead of option arguments. This has the benefit of supporting shell wildcards. Old usage: checksec -f /bin/true --no-color checksec -d /bin --json --pretty checksec -p bash --maps checksec --pid 1,42 checksec -P New usage: checksec --no-color exe /bin/true checksec --format json-pretty exe /bin checksec proc-name --maps bash checksec proc-id 1 42 checksec proc-all checksec proc-id $(pidof firefox) checksec exe /bin/system* dpkg -L apt | checksec --- Cargo.lock | 51 ++++++ Cargo.toml | 3 +- src/main.rs | 425 ++++++++++++++++++++++++++------------------------ src/output.rs | 23 ++- 4 files changed, 295 insertions(+), 207 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 395b24e..ae91cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "checksec" version = "0.0.9" dependencies = [ + "atty", "clap", "colored", "colored_json", @@ -76,12 +77,26 @@ checksum = "6ea54a38e4bce14ff6931c72e5b3c43da7051df056913d4e7e1fcdb1c03df69d" dependencies = [ "atty", "bitflags", + "clap_derive", "clap_lex", "once_cell", "strsim", "termcolor", ] +[[package]] +name = "clap_derive" +version = "4.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.3.0" @@ -199,6 +214,12 @@ dependencies = [ "scroll", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -314,6 +335,30 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.46" @@ -501,6 +546,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 8df234a..1e0a627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ opt-level = 'z' # Optimize for size panic = 'abort' # Abort on panic [dependencies] -clap = {version = "4.0.14", features = ["cargo"]} +atty = "0.2.14" +clap = {version = "4.0.14", features = ["cargo", "derive"]} colored = {version = "2.0.0", optional = true} colored_json = {version = "3.0.1", optional = true} goblin = "0.5.4" diff --git a/src/main.rs b/src/main.rs index 23c26c0..ee1a779 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,10 @@ extern crate ignore; extern crate serde_json; extern crate sysinfo; -use clap::{ - crate_authors, crate_description, crate_version, Arg, ArgAction, ArgGroup, - Command, -}; +use clap::CommandFactory; +use clap::Parser; +use clap::Subcommand; +use clap::{arg, command}; use goblin::error::Error; #[cfg(feature = "macho")] use goblin::mach::Mach; @@ -20,8 +20,9 @@ use sysinfo::{ PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, }; +use std::io::BufRead; use std::path::{Path, PathBuf}; -use std::{env, fmt, fs, process}; +use std::{fmt, fs, io, process}; #[cfg(feature = "color")] use colored::Colorize; @@ -73,7 +74,11 @@ fn print_binary_results(binaries: &Binaries, settings: &output::Settings) { } } -fn print_process_results(processes: &Processes, settings: &output::Settings) { +fn print_process_results( + processes: &Processes, + settings: &output::Settings, + maps: bool, +) { match settings.format { output::Format::Json => { println!("{}", &json!(processes)); @@ -112,7 +117,7 @@ fn print_process_results(processes: &Processes, settings: &output::Settings) { feature = "maps", any(target_os = "linux", target_os = "windows") ))] - if settings.maps { + if maps { if let Some(maps) = &process.maps { println!("{:>12}", "\u{21aa} Maps:"); for map in maps { @@ -230,237 +235,257 @@ fn parse(file: &Path) -> Result, ParseError> { } } -fn walk(basepath: &Path, settings: &output::Settings) { - let mut bins: Vec = Vec::new(); - for result in Walk::new(basepath).flatten() { - if let Some(filetype) = result.file_type() { - if filetype.is_file() { - if let Ok(mut result) = parse(result.path()) { - bins.append(&mut result); - } - } - } - } - print_binary_results(&Binaries::new(bins), settings); +#[derive(Debug, Parser)] +#[command(author, version, about, override_usage = "checksec [OPTIONS] [COMMAND]\n command | checksec [OPTIONS]")] +struct Cli { + #[command(subcommand)] + command: Option, + /// Disables color output + #[arg(long = "no-color")] + color: bool, + /// Output format + #[arg(long, default_value_t = output::Format::Text)] + format: output::Format, } -#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] + +#[derive(Debug, Subcommand)] +enum Commands { + /// Scan executables by path + #[command(arg_required_else_help = true)] + Exe { + #[arg(required = true)] + paths: Vec, + }, + /// Scan processes by PID + #[command(arg_required_else_help = true)] + ProcID { + #[arg(required = true)] + pids: Vec, + /// Include process memory maps (linux only) + #[arg(short, long)] + maps: bool, + }, + /// Scan processes by name + #[command(arg_required_else_help = true)] + ProcName { + #[arg(required = true)] + procnames: Vec, + /// Include process memory maps (linux only) + #[arg(short, long)] + maps: bool, + }, + /// Scan all running processes + ProcAll { + /// Include process memory maps (linux only) + #[arg(short, long)] + maps: bool, + }, +} + fn main() { - let args = Command::new("checksec") - .about(crate_description!()) - .author(crate_authors!()) - .version(crate_version!()) - .arg_required_else_help(true) - .arg( - Arg::new("directory") - .short('d') - .long("directory") - .value_name("DIRECTORY") - .help("Target directory"), - ) - .arg( - Arg::new("file") - .short('f') - .long("file") - .value_name("FILE") - .help("Target file"), - ) - .arg( - Arg::new("json") - .short('j') - .long("json") - .action(ArgAction::SetTrue) - .help("Output in json format"), - ) - .arg( - Arg::new("maps") - .short('m') - .long("maps") - .action(ArgAction::SetTrue) - .help("Include process memory maps (linux only)") - .requires("pid") - .requires("process") - .requires("process-all") - .conflicts_with_all(&["directory", "file"]), - ) - .arg( - Arg::new("no-color") - .long("no-color") - .action(ArgAction::SetTrue) - .help("Disables color output"), - ) - .arg( - Arg::new("pid") - .help( - "Process ID of running process to check\n\ - (comma separated for multiple PIDs)", - ) - .long("pid") - .value_name("PID"), - ) - .arg( - Arg::new("pretty") - .long("pretty") - .action(ArgAction::SetTrue) - .help("Human readable json output") - .requires("json"), - ) - .arg( - Arg::new("process") - .short('p') - .long("process") - .value_name("NAME") - .help("Name of running process to check"), - ) - .arg( - Arg::new("process-all") - .short('P') - .long("process-all") - .action(ArgAction::SetTrue) - .help("Check all running processes"), - ) - .group( - ArgGroup::new("operation") - .args(&["directory", "file", "pid", "process", "process-all"]) - .required(true), - ) - .get_matches(); - - let file = args.get_one::("file"); - let directory = args.get_one::("directory"); - let procids = args.get_one::("pid"); - let procname = args.get_one::("process"); - let procall = args.get_flag("process-all"); - - let format = if args.get_flag("json") { - if args.get_flag("pretty") { - output::Format::JsonPretty - } else { - output::Format::Json - } - } else { - output::Format::Text - }; + let args = Cli::parse(); + + let format = args.format; let settings = output::Settings::set( #[cfg(feature = "color")] - !args.get_flag("no-color"), + !args.color, format, - args.get_flag("maps"), ); - if procall { - let system = System::new_with_specifics( - RefreshKind::new() - .with_processes(ProcessRefreshKind::new().with_cpu()), - ); - let mut procs: Vec = Vec::new(); - for (pid, proc_entry) in system.processes() { - if let Ok(results) = parse(proc_entry.exe()) { - procs.push(Process::new(pid.as_u32() as usize, results)); + match args.command { + Some(Commands::Exe { paths }) => { + let results = scan_paths(&paths); + print_binary_results(&Binaries::new(results), &settings); + } + Some(Commands::ProcID { pids, maps }) => { + let results = scan_pids(&pids); + if results.is_empty() { + process::exit(1); } + print_process_results(&Processes::new(results), &settings, maps); } - print_process_results(&Processes::new(procs), &settings); - } else if let Some(procids) = procids { - let procids: Vec = procids - .split(',') - .map(|id| match id.parse::() { - Ok(id) => id, - Err(msg) => { - eprintln!("Invalid process ID {}: {}", id, msg); - process::exit(1); - } - }) - .collect(); - let system = System::new_with_specifics( - RefreshKind::new() - .with_processes(ProcessRefreshKind::new().with_cpu()), - ); + Some(Commands::ProcName { procnames, maps }) => { + let results = scan_procnames(&procnames); + if results.is_empty() { + process::exit(1); + } + print_process_results(&Processes::new(results), &settings, maps); + } + Some(Commands::ProcAll { maps }) => { + let results = scan_all_processes(); + if results.is_empty() { + eprintln!("No running process found"); + process::exit(1); + } + print_process_results(&Processes::new(results), &settings, maps); + } + None => { + #[allow(unused_must_use)] + if atty::is(atty::Stream::Stdin) { + let mut cmd = Cli::command(); + cmd.print_help(); + process::exit(1); + } - let mut procs: Vec = Vec::new(); - for procid in procids { - let process = if let Some(process) = system.process(procid) { - process - } else { - eprintln!("No process found with ID {}", procid); - continue; - }; + let results = io::stdin() + .lock() + .lines() + .map(|line| { + line.expect("Cannot read line from standard input") + }) + .filter_map(|file| { + let path = Path::new(&file); + if path.is_file() { + parse(path).ok() + } else { + None + } + }) + .flatten() + .collect(); + print_binary_results(&Binaries::new(results), &settings); + } + }; +} + +fn scan_paths(paths: &[PathBuf]) -> Vec { + let mut results = Vec::new(); - if !process.exe().is_file() { + for path in paths { + let metadata = match fs::metadata(path) { + Ok(m) => m, + Err(e) => { eprintln!( - "No valid executable found for process {} with ID {}: {}", - process.name(), - procid, - process.exe().display() + "Failed to check path {}: {}", + underline!(path.display().to_string()), + e ); continue; } + }; - match parse(process.exe()) { - Ok(results) => { - procs - .push(Process::new(procid.as_u32() as usize, results)); - } + if metadata.is_file() { + match parse(path) { + Ok(mut res) => results.append(&mut res), Err(msg) => { eprintln!( - "Can not parse process {} with ID {}: {}", - process.name(), - procid, + "Cannot parse binary file {}: {}", + underline!(path.display().to_string()), msg ); - continue; } } + continue; } - print_process_results(&Processes::new(procs), &settings); - } else if let Some(procname) = procname { - let system = System::new_with_specifics( - RefreshKind::new() - .with_processes(ProcessRefreshKind::new().with_cpu()), - ); - let sysprocs = system.processes_by_name(procname); - let mut procs: Vec = Vec::new(); - for proc_entry in sysprocs { - if let Ok(results) = parse(proc_entry.exe()) { - procs.push(Process::new( - proc_entry.pid().as_u32() as usize, - results, - )); + + if metadata.is_dir() { + for entry in Walk::new(path).flatten() { + if let Some(filetype) = entry.file_type() { + if filetype.is_file() { + if let Ok(mut res) = parse(entry.path()) { + results.append(&mut res); + } + } + } } + continue; } - if procs.is_empty() { - eprintln!("No process found matching name {}", procname); - process::exit(1); - } - print_process_results(&Processes::new(procs), &settings); - } else if let Some(directory) = directory { - let directory_path = Path::new(directory); - if !directory_path.is_dir() { - eprintln!("Directory {} not found", underline!(directory)); - process::exit(1); - } + eprintln!( + "{} is an unsupported type of file", + underline!(path.display().to_string()) + ); + } - walk(directory_path, &settings); - } else if let Some(file) = file { - let file_path = Path::new(file); + results +} + +fn scan_pids(pids: &[sysinfo::Pid]) -> Vec { + let system = System::new_with_specifics( + RefreshKind::new() + .with_processes(ProcessRefreshKind::new().with_cpu()), + ); + + let mut procs = Vec::new(); + for procid in pids { + let process = if let Some(process) = system.process(*procid) { + process + } else { + eprintln!("No process found with ID {}", procid); + continue; + }; - if !file_path.is_file() { - eprintln!("File {} not found", underline!(file)); - process::exit(1); + if !process.exe().is_file() { + eprintln!( + "No valid executable found for process {} with ID {}: {}", + process.name(), + procid, + process.exe().display() + ); + continue; } - match parse(file_path) { + match parse(process.exe()) { Ok(results) => { - print_binary_results(&Binaries::new(results), &settings); + procs.push(Process::new(procid.as_u32() as usize, results)); } Err(msg) => { eprintln!( - "Cannot parse binary file {}: {}", - underline!(file), + "Can not parse process {} with ID {}: {}", + process.name(), + procid, msg ); - process::exit(1); + continue; + } + } + } + + procs +} + +fn scan_procnames(procnames: &[String]) -> Vec { + let system = System::new_with_specifics( + RefreshKind::new() + .with_processes(ProcessRefreshKind::new().with_cpu()), + ); + + let mut procs = Vec::new(); + + for procname in procnames { + let mut found = false; + for proc_entry in system.processes_by_name(procname) { + found = true; + if let Ok(results) = parse(proc_entry.exe()) { + procs.push(Process::new( + proc_entry.pid().as_u32() as usize, + results, + )); } } + + if !found { + eprintln!("No process found with name {}", procname); + continue; + } } + + procs +} + +fn scan_all_processes() -> Vec { + let system = System::new_with_specifics( + RefreshKind::new() + .with_processes(ProcessRefreshKind::new().with_cpu()), + ); + + system + .processes() + .iter() + .filter_map(|(pid, proc_entry)| match parse(proc_entry.exe()) { + Ok(res) => Some(Process::new(pid.as_u32() as usize, res)), + Err(_) => None, + }) + .collect() } diff --git a/src/output.rs b/src/output.rs index 6898302..4ce0bd2 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,38 +1,49 @@ +use clap::ValueEnum; #[cfg(feature = "color")] use colored::control; #[cfg(feature = "color")] use std::env; +#[derive(Clone, Debug, ValueEnum)] pub enum Format { Text, Json, JsonPretty, } +impl std::fmt::Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text => write!(f, "text"), + Self::Json => write!(f, "json"), + Self::JsonPretty => write!(f, "json (pretty)"), + } + } +} + pub struct Settings { #[cfg(feature = "color")] pub color: bool, pub format: Format, - pub maps: bool, } impl Settings { #[must_use] #[cfg(feature = "color")] - pub fn set(color: bool, format: Format, maps: bool) -> Self { + pub fn set(color: bool, format: Format) -> Self { if color { // honor NO_COLOR if it is set within the environment if env::var("NO_COLOR").is_ok() { - return Self { color: false, format, maps }; + return Self { color: false, format }; } } else { control::set_override(false); } - Self { color, format, maps } + Self { color, format } } #[must_use] #[cfg(not(feature = "color"))] - pub fn set(format: Format, maps: bool) -> Self { - Self { format, maps } + pub fn set(format: Format) -> Self { + Self { format } } }