boreal-cli 1.1.0

CLI utility to run boreal, a YARA rules engine
use boreal::compiler::{CompilerProfile, ExternalValue};
use clap::parser::Values;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};

#[derive(Debug)]
pub struct CompilerOptions {
    pub profile: Option<CompilerProfile>,
    pub compute_statistics: bool,
    pub max_strings_per_rule: Option<usize>,
    pub max_condition_depth: Option<u32>,
    pub parse_expr_recursion_limit: Option<u8>,
    pub parse_string_recursion_limit: Option<u8>,
    pub disable_includes: bool,
    pub defines: Option<Values<(String, ExternalValue)>>,
    pub rules_files: Vec<String>,
}

impl CompilerOptions {
    pub fn from_args(args: &mut ArgMatches, rules_files: Option<Vec<String>>) -> Self {
        // In the yr subcommand, rules_file are part of the positional arguments,
        // and are parsed differently.
        let rules_files =
            rules_files.unwrap_or_else(|| match args.remove_many::<String>("rules_file") {
                Some(v) => v.into_iter().collect(),
                None => Vec::new(),
            });

        Self {
            profile: args.remove_one::<CompilerProfile>("profile"),
            compute_statistics: args.get_flag("string_statistics"),
            max_strings_per_rule: args.remove_one::<usize>("max_strings_per_rule"),
            max_condition_depth: args.remove_one::<u32>("max_condition_depth"),
            parse_expr_recursion_limit: args.remove_one::<u8>("parse_expr_recursion_limit"),
            parse_string_recursion_limit: args.remove_one::<u8>("parse_string_recursion_limit"),
            disable_includes: args.get_flag("disable_includes"),
            defines: args.remove_many::<(String, ExternalValue)>("define"),
            rules_files,
        }
    }
}

pub fn add_compiler_args(mut command: Command, in_yr_subcommand: bool) -> Command {
    command = command
        .next_help_heading("Compilation options")
        .arg(
            Arg::new("profile")
                .long("profile")
                .value_name("speed|memory")
                .value_parser(parse_compiler_profile)
                .help("Profile to use when compiling rules"),
        )
        .arg(
            Arg::new("string_statistics")
                .long("string-stats")
                .action(ArgAction::SetTrue)
                .help("Display statistics on rules' compilation"),
        )
        .arg(
            Arg::new("max_strings_per_rule")
                .long("max-strings-per-rule")
                .value_name("NUMBER")
                .value_parser(value_parser!(usize))
                .help("Maximum number of strings in a single rule"),
        )
        .arg(
            Arg::new("max_condition_depth")
                .long("max-condition-depth")
                .value_name("NUMBER")
                .value_parser(value_parser!(u32))
                .help("Maximum depth in a rule's condition AST")
                .long_help(
                    "Maximum depth in a rule's condition AST.\n\n\
                    Defensive limit used to prevent stack overflow.\n\
                    Should this limit be reached when compiling rules, this parameter \
                    can be used to modify it.",
                ),
        )
        .arg(
            Arg::new("parse_expr_recursion_limit")
                .long("parse-expression-recursion-limit")
                .value_name("NUMBER")
                .value_parser(value_parser!(u8))
                .help("Maximum recursion depth allowed when parsing an expression")
                .long_help(
                    "Maximum recursion depth allowed when parsing an expression.\n\n\
                    Defensive limit used to prevent stack overflow.\n\
                    Should this limit be reached when compiling rules, this parameter \
                    can be used to modify it.",
                ),
        )
        .arg(
            Arg::new("parse_string_recursion_limit")
                .long("parse-string-recursion-limit")
                .value_name("NUMBER")
                .value_parser(value_parser!(u8))
                .help("Maximum recursion depth allowed when parsing a regex or a hex-string")
                .long_help(
                    "Maximum recursion depth allowed when parsing a regex or a hex-string.\n\n\
                    Defensive limit used to prevent stack overflow.\n\
                    Should this limit be reached when compiling rules, this parameter \
                    can be used to modify it.",
                ),
        )
        .arg(
            Arg::new("disable_includes")
                .long("disable-includes")
                .action(ArgAction::SetTrue)
                .help("Disable the possibility to include yara files"),
        )
        .arg(
            Arg::new("define")
                .short('d')
                .long("define")
                .value_name("VAR=VALUE")
                .action(ArgAction::Append)
                .value_parser(parse_define)
                .help("Define a symbol that can be used in rules"),
        );

    if !in_yr_subcommand {
        command = command.arg(
            Arg::new("rules_file")
                .short('f')
                .long("rules-file")
                .value_name("[NAMESPACE:]RULES_FILE")
                .action(ArgAction::Append)
                .help("Path to a file containing rules, can be repeated.")
                .long_help(
                    "Path to a file containing rules, can be repeated.\n\n\
                     The path can be prefixed by the namespace in which to \
                     compile the rules, followed by a colon.\n\
                     This can notably be used to avoid name collisions when \
                     using multiple rules files.",
                ),
        );
    }

    command
}

fn parse_compiler_profile(profile: &str) -> Result<CompilerProfile, String> {
    match profile {
        "speed" => Ok(CompilerProfile::Speed),
        "memory" => Ok(CompilerProfile::Memory),
        _ => Err("invalid value".to_string()),
    }
}

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

    let external_value = if value == "true" {
        ExternalValue::Boolean(true)
    } else if value == "false" {
        ExternalValue::Boolean(false)
    } else if value.contains('.') {
        match value.parse::<f64>() {
            Ok(v) => ExternalValue::Float(v),
            Err(_) => ExternalValue::Bytes(value.as_bytes().to_vec()),
        }
    } else {
        match value.parse::<i64>() {
            Ok(v) => ExternalValue::Integer(v),
            Err(_) => ExternalValue::Bytes(value.as_bytes().to_vec()),
        }
    };

    Ok((name.to_owned(), external_value))
}