From 174f78f27d6382a837889d20bc45d26b8f890972 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 | 461 +++++++++++++++++++++++++------------------------- src/output.rs | 28 +-- 4 files changed, 303 insertions(+), 240 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bcdd8c..23b7cf7 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", @@ -82,12 +83,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" @@ -211,6 +226,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" @@ -344,6 +365,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" @@ -529,6 +574,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 c569df9..2acc95d 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} either = "1.8.1" diff --git a/src/main.rs b/src/main.rs index ca8660c..cf9ed18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,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}; #[cfg(all(feature = "maps", target_os = "linux"))] use either::Either; use goblin::error::Error; @@ -33,13 +33,12 @@ use std::collections::HashMap; #[cfg(all(target_os = "linux", feature = "elf"))] use std::collections::HashSet; use std::ffi::OsStr; -use std::io::ErrorKind; +use std::io::{BufRead, ErrorKind}; #[cfg(all(feature = "color", not(target_os = "windows")))] use std::os::unix::fs::PermissionsExt; -use std::path::Path; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use std::{env, fmt, fs, process}; +use std::{fmt, fs, process}; #[cfg(feature = "color")] use colored::{ColoredString, Colorize}; @@ -165,7 +164,11 @@ fn print_binary_results(binaries: &[Binary], 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)); @@ -233,7 +236,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 { @@ -595,11 +598,7 @@ fn parse_single_file( parse_file_impl(file, true, &Some(lookup), &mut None) } -fn walk( - basepath: &Path, - scan_dynlibs: bool, - output_settings: &output::Settings, -) { +fn walk(basepath: &Path, scan_dynlibs: bool) -> Vec { let lookup = if scan_dynlibs { Some(Lookup { #[cfg(all(target_os = "linux", feature = "elf"))] @@ -614,7 +613,7 @@ fn walk( let cache = Arc::new(Mutex::new(HashMap::new())); - let bins: Vec = Walk::new(basepath) + Walk::new(basepath) .flatten() .filter(|entry| { entry.file_type().filter(std::fs::FileType::is_file).is_some() @@ -630,9 +629,7 @@ fn walk( .ok() }) .flatten() - .collect(); - - print_binary_results(&bins, output_settings); + .collect() } #[cfg(all(feature = "maps", target_os = "linux"))] @@ -700,7 +697,11 @@ fn parse_process_libraries( )) } -fn parse_processes<'a, I>(processes: I, scan_dynlibs: bool) -> Vec +fn parse_processes<'a, I>( + processes: I, + quiet: bool, + scan_dynlibs: bool, +) -> Vec where I: Iterator + Send, { @@ -711,11 +712,13 @@ where .filter_map(|process| { match parse(process.exe(), &mut Some(Arc::clone(&cache))) { Err(err) => { - if let ParseError::IO(ref e) = err { - if e.kind() == ErrorKind::NotFound - || e.kind() == ErrorKind::PermissionDenied - { - return None; + if quiet { + if let ParseError::IO(ref e) = err { + if e.kind() == ErrorKind::NotFound + || e.kind() == ErrorKind::PermissionDenied + { + return None; + } } } @@ -753,228 +756,230 @@ where .collect() } -#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] +#[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, + /// Scan shared libraries + #[arg(short, long)] + libraries: bool, +} + +#[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("libraries") - .short('l') - .long("libraries") - .action(ArgAction::SetTrue) - .help("Include all shared loaded libraries (Linux only)") - .requires("directory") - .requires("file") - .requires("pid") - .requires("process") - .requires("process-all"), - ) - .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(); - - // required operation - 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"); - - // optional modifiers - let libraries = args.get_flag("libraries"); - - 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"), - libraries, + args.libraries, ); - if procall { - let system = System::new_with_specifics( - RefreshKind::new() - .with_processes(ProcessRefreshKind::new().with_cpu()), - ); - - let procs = parse_processes(system.processes().values(), libraries); - - 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()), - ); + match args.command { + Some(Commands::Exe { paths }) => { + let results = scan_paths(&paths, args.libraries); + print_binary_results(&results, &settings); + } + Some(Commands::ProcID { pids, maps }) => { + let results = scan_pids(&pids, args.libraries); + if results.is_empty() { + process::exit(1); + } + print_process_results(&Processes::new(results), &settings, maps); + } + Some(Commands::ProcName { procnames, maps }) => { + let results = scan_procnames(&procnames, args.libraries); + if results.is_empty() { + process::exit(1); + } + print_process_results(&Processes::new(results), &settings, maps); + } + Some(Commands::ProcAll { maps }) => { + let results = scan_all_processes(args.libraries); + 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 procs = parse_processes( - procids - .iter() - .filter_map(|&pid| { - system.process(pid).or_else(|| { - eprintln!("No process found with ID {pid}"); - None - }) + let results: Vec = std::io::stdin() + .lock() + .lines() + .map(|line| { + line.expect("Cannot read line from standard input") }) - .filter(|process| { - if process.exe().is_file() { - true + .filter_map(|file| { + let path = Path::new(&file); + if path.is_file() { + parse_single_file(path, args.libraries).ok() } else { - eprintln!( - "No valid executable found for process {} with ID {}: {}", - process.name(), - process.pid(), - process.exe().display() - ); - false + None } - }), - libraries, - ); + }) + .flatten() + .collect(); + print_binary_results(&results, &settings); + } + }; +} - 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()), - ); +fn scan_paths(paths: &[PathBuf], libraries: bool) -> Vec { + let mut results = Vec::new(); - let procs = parse_processes( - system - .processes_by_name(procname) - // TODO: processes_by_name() should return a Iterator implementing the trait Send - .collect::>() - .into_iter(), - libraries, - ); + for path in paths { + let metadata = match fs::metadata(path) { + Ok(m) => m, + Err(e) => { + eprintln!( + "Failed to check path {}: {}", + underline!(path.display().to_string()), + e + ); + continue; + } + }; - if procs.is_empty() { - eprintln!("No process found matching name {procname}"); - process::exit(1); + if metadata.is_file() { + // TODO: reuse cache + match parse_single_file(path, libraries) { + Ok(mut res) => results.append(&mut res), + Err(msg) => { + eprintln!( + "Cannot parse binary file {}: {}", + underline!(path.display().to_string()), + msg + ); + } + } + continue; } - 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); + if metadata.is_dir() { + // TODO: reuse cache + results.append(&mut walk(path, libraries)); + continue; } - walk(directory_path, libraries, &settings); - } else if let Some(file) = file { - let file_path = Path::new(file); + eprintln!( + "{} is an unsupported type of file", + underline!(path.display().to_string()) + ); + } - if !file_path.is_file() { - eprintln!("File {} not found", underline!(file)); - process::exit(1); - } + results +} - match parse_single_file(file_path, libraries) { - Ok(result) => { - print_binary_results(&result, &settings); - } - Err(msg) => { - eprintln!( - "Cannot parse binary file {}: {}", - underline!(file), - msg - ); - process::exit(1); +fn scan_pids(pids: &[sysinfo::Pid], libraries: bool) -> Vec { + let system = System::new_with_specifics( + RefreshKind::new() + .with_processes(ProcessRefreshKind::new().with_cpu()), + ); + + parse_processes( + pids.iter().filter_map(|pid| { + if let Some(process) = system.process(*pid) { + Some(process) + } else { + eprintln!("No process found with ID {pid}"); + None } - } - } + }), + false, + libraries, + ) +} + +fn scan_procnames(procnames: &[String], libraries: bool) -> Vec { + let system = System::new_with_specifics( + RefreshKind::new() + .with_processes(ProcessRefreshKind::new().with_cpu()), + ); + + parse_processes( + procnames + .iter() + .filter_map(|procname| { + // processes_by_name() returns an Iterator not implementing Send + let procs: Vec<_> = + system.processes_by_name(procname).collect(); + if procs.is_empty() { + eprintln!("No process found with name {procname}"); + None + } else { + Some(procs) + } + }) + .flatten(), + false, + libraries, + ) +} + +fn scan_all_processes(libraries: bool) -> Vec { + let system = System::new_with_specifics( + RefreshKind::new() + .with_processes(ProcessRefreshKind::new().with_cpu()), + ); + + parse_processes( + system.processes().iter().map(|(_pid, process)| process), + true, + libraries, + ) } diff --git a/src/output.rs b/src/output.rs index 42d91b9..98faa0d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,44 +1,50 @@ +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, pub libraries: bool, } impl Settings { #[must_use] #[cfg(feature = "color")] - pub fn set( - color: bool, - format: Format, - maps: bool, - libraries: bool, - ) -> Self { + pub fn set(color: bool, format: Format, libraries: bool) -> 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, libraries }; + return Self { color: false, format, libraries }; } } else { control::set_override(false); } - Self { color, format, maps, libraries } + Self { color, format, libraries } } #[must_use] #[cfg(not(feature = "color"))] - pub fn set(format: Format, maps: bool, libraries: bool) -> Self { - Self { format, maps, libraries } + pub fn set(format: Format, libraries: bool) -> Self { + Self { format, libraries } } }