boreal-cli 1.1.0

CLI utility to run boreal, a YARA rules engine
use std::path::PathBuf;

use boreal::scanner::FragmentedScanMode;
use clap::parser::Values;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};

#[derive(Debug)]
pub struct ScannerOptions {
    pub memory_chunk_size: Option<usize>,
    pub timeout: Option<u64>,
    pub max_fetched_region_size: Option<usize>,
    pub fragmented_scan_mode: Option<FragmentedScanMode>,
    pub string_max_nb_matches: Option<u32>,
    pub module_data: Option<Values<(String, PathBuf)>>,
    pub no_console_logs: bool,
}

impl ScannerOptions {
    pub fn from_args(args: &mut ArgMatches) -> Self {
        Self {
            memory_chunk_size: args.remove_one("memory_chunk_size"),
            timeout: args.remove_one("timeout"),
            max_fetched_region_size: args.remove_one("max_fetched_region_size"),
            fragmented_scan_mode: args.remove_one("fragmented_scan_mode"),
            string_max_nb_matches: args.remove_one("string_max_nb_matches"),
            module_data: args.remove_many::<(String, PathBuf)>("module_data"),
            no_console_logs: args.get_flag("no_console_logs"),
        }
    }
}

pub fn add_scanner_args(command: Command) -> Command {
    command
        .next_help_heading("Scanning options")
        .arg(
            Arg::new("memory_chunk_size")
                .long("max-process-memory-chunk")
                .value_name("NUMBER")
                .value_parser(value_parser!(usize))
                .help("Maximum chunk size when scanning processes"),
        )
        .arg(
            Arg::new("timeout")
                .short('a')
                .long("timeout")
                .value_name("SECONDS")
                .value_parser(value_parser!(u64))
                .help("Set the timeout duration before scanning is aborted"),
        )
        .arg(
            Arg::new("max_fetched_region_size")
                .long("max-fetched-region-size")
                .value_name("NUMBER")
                .value_parser(value_parser!(usize))
                .help("Maximum size fetched from a process region"),
        )
        .arg(
            Arg::new("fragmented_scan_mode")
                .long("fragmented-scan-mode")
                .value_name("legacy|fast|singlepass")
                .value_parser(parse_fragmented_scan_mode)
                .help("Specify scan mode for fragmented memory (e.g. process scanning)"),
        )
        .arg(
            Arg::new("string_max_nb_matches")
                .long("string-max-nb-matches")
                .value_name("NUMBER")
                .value_parser(value_parser!(u32))
                .help("Maximum number of matches for a single string, default is 1000"),
        )
        .arg(
            Arg::new("module_data")
                .short('x')
                .long("module-data")
                .value_name("MODULE=FILE")
                .action(ArgAction::Append)
                .value_parser(parse_module_data)
                .help("Specify the data to use in a module")
                .long_help(
                    "Specify the data to use in a module.\n\n\
                     Note that only the cuckoo module is supported.",
                ),
        )
        .arg(
            Arg::new("no_console_logs")
                .short('q')
                .long("disable-console-logs")
                .action(ArgAction::SetTrue)
                .help("Disable printing console log messages"),
        )
}

fn parse_fragmented_scan_mode(scan_mode: &str) -> Result<FragmentedScanMode, String> {
    match scan_mode {
        "legacy" => Ok(FragmentedScanMode::legacy()),
        "fast" => Ok(FragmentedScanMode::fast()),
        "singlepass" => Ok(FragmentedScanMode::single_pass()),
        _ => Err("invalid value".to_string()),
    }
}

fn parse_module_data(arg: &str) -> Result<(String, PathBuf), String> {
    let Some((name, path)) = arg.split_once('=') else {
        return Err("missing '=' delimiter".to_owned());
    };

    Ok((name.to_owned(), PathBuf::from(path)))
}