mod check;
mod compile;
mod completion;
mod debug;
mod deps;
mod dump;
mod fix;
mod fmt;
mod scan;
pub use check::*;
pub use compile::*;
pub use completion::*;
#[cfg(feature = "debug-cmd")]
pub use debug::*;
pub use deps::*;
pub use dump::*;
pub use fix::*;
pub use fmt::*;
pub use scan::*;
use std::borrow::Cow;
use std::fs;
use std::io::stdout;
use std::path::PathBuf;
use anyhow::{Context, anyhow, bail};
use clap::{
Arg, ArgAction, ArgMatches, Command, arg, command, crate_authors,
value_parser,
};
use crossterm::tty::IsTty;
use indicatif::{ProgressBar, ProgressStyle};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use yansi::Color::Green;
use yansi::Paint;
use crate::config::Config;
use crate::walk::Draw;
use crate::walk::Walker;
use crate::{APP_HELP_TEMPLATE, commands, help};
use yara_x::{Compiler, Rules, SourceCode};
pub fn command(name: &'static str) -> Command {
Command::new(name).help_template(
r#"{about-with-newline}
{usage-heading}
{usage}
{all-args}
"#,
)
}
pub fn cli() -> Command {
command!()
.author(crate_authors!("\n")) .arg_required_else_help(true)
.arg(
arg!(-C --config <CONFIG_FILE> "Config file")
.value_parser(existing_path_parser)
.long_help(help::CONFIG_FILE),
)
.help_template(APP_HELP_TEMPLATE)
.subcommand_required(true)
.subcommands(vec![
commands::scan(),
commands::compile(),
commands::check(),
#[cfg(feature = "debug-cmd")]
commands::debug(),
commands::dump(),
commands::fmt(),
commands::fix(),
commands::completion(),
commands::deps(),
])
}
fn get_external_vars(
args: &ArgMatches,
) -> Option<Vec<(String, serde_json::Value)>> {
args.get_many::<(String, serde_json::Value)>("define")
.map(|var| var.cloned().collect())
}
fn external_var_parser(
option: &str,
) -> Result<(String, serde_json::Value), anyhow::Error> {
let (var, value) = option.split_once('=').ok_or(anyhow!(
"the equal sign is missing, use the syntax VAR=VALUE (example: {option}=10)",
))?;
let value = serde_json::from_str(value).map_err(|_| {
anyhow!(
"`{value}` is not a valid value, did you mean \\\"{value}\\\"?"
)
})?;
Ok((var.to_string(), value))
}
fn meta_file_value_parser(
option: &str,
) -> Result<(String, PathBuf), anyhow::Error> {
let (var, value) = option.split_once('=').ok_or(anyhow!(
"the equal sign is missing, use the syntax MODULE=FILE (example: {option}=file)"
))?;
let value = PathBuf::from(value);
Ok((var.to_string(), value))
}
fn path_with_namespace_parser(
input: &str,
) -> Result<(Option<String>, PathBuf), anyhow::Error> {
if let Some((namespace, path)) = input.split_once(':') {
#[cfg(target_os = "windows")]
if namespace.len() == 1 {
return Ok((None, PathBuf::from(input)));
}
Ok((Some(namespace.to_string()), PathBuf::from(path)))
} else {
Ok((None, PathBuf::from(input)))
}
}
fn existing_path_parser(input: &str) -> Result<PathBuf, anyhow::Error> {
let path = PathBuf::from(input);
if path.try_exists()? { Ok(path) } else { Err(anyhow!("file not found")) }
}
pub fn create_compiler<'a>(
external_vars: Option<Vec<(String, serde_json::Value)>>,
args: &ArgMatches,
config: &Config,
) -> Result<Compiler<'a>, anyhow::Error> {
let mut compiler = Compiler::new();
compiler
.relaxed_re_syntax(
args.try_get_one::<bool>("relaxed-re-syntax")
.unwrap_or_default()
.cloned()
.unwrap_or_default(),
)
.colorize_errors(stdout().is_tty());
for module in args
.try_get_many::<String>("ignore-module")
.unwrap_or_default()
.into_iter()
.flatten()
{
compiler.ignore_module(module);
}
for dir in args
.try_get_many::<PathBuf>("include-dir")
.unwrap_or_default()
.into_iter()
.flatten()
{
compiler.add_include_dir(dir);
}
let disabled_warnings: Vec<_> = args
.try_get_many::<String>("disable-warnings")
.unwrap_or_default()
.into_iter()
.flatten()
.chain(config.warnings.iter().filter_map(
|(warning_id, warning_config)| {
warning_config.disabled.then_some(warning_id)
},
))
.collect();
if disabled_warnings.iter().any(|w| *w == "all") {
compiler.switch_all_warnings(false);
} else {
for warning in &disabled_warnings {
compiler.switch_warning(warning, false)?;
}
}
if let Some(vars) = external_vars {
for (ident, value) in vars {
compiler.define_global(ident.as_str(), value)?;
}
}
Ok(compiler)
}
pub fn compilation_args() -> [Arg; 6] {
[
arg!(-d --"define")
.help("Define external variable")
.long_help(help::DEFINE_LONG_HELP)
.value_name("VAR=VALUE")
.value_parser(external_var_parser)
.action(ArgAction::Append),
arg!(-w --"disable-warnings" [WARNING_ID])
.help("Disable warnings")
.long_help(help::DISABLE_WARNINGS_LONG_HELP)
.default_missing_value("all")
.num_args(0..)
.require_equals(true)
.value_delimiter(',')
.action(ArgAction::Append),
arg!(-I --"ignore-module" <MODULE>)
.help("Ignore rules that use the specified module")
.long_help(help::IGNORE_MODULE_LONG_HELP)
.action(ArgAction::Append),
arg!(--"include-dir" <PATH>)
.help("Directory in which to search for included files")
.long_help(help::INCLUDE_DIR_LONG_HELP)
.value_parser(value_parser!(PathBuf))
.action(ArgAction::Append),
arg!(--"path-as-namespace")
.help("Use file path as rule namespace"),
arg!(--"relaxed-re-syntax")
.help("Use a more relaxed syntax check while parsing regular expressions"),
]
}
pub fn compile_rules<'a, P>(
paths: P,
args: &ArgMatches,
config: &Config,
) -> Result<Rules, anyhow::Error>
where
P: Iterator<Item = &'a (Option<String>, PathBuf)>,
{
let external_vars = get_external_vars(args);
let mut compiler = create_compiler(external_vars, args, config)?;
let mut pb = if stdout().is_tty() {
let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner().template("{msg}")?);
Some(pb)
} else {
None
};
let mut state = CompileState::new();
for (namespace, path) in paths {
let mut w = Walker::path(path);
w.filter("**/*.yar");
w.filter("**/*.yara");
compiler.new_namespace(
namespace
.as_ref()
.map(|namespace| namespace.as_str())
.unwrap_or("default"),
);
if let Err(err) = w.walk(
|file_path| {
state.file_in_progress = Some(file_path.into());
if let Some(pb) = pb.as_mut() {
let width = crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80);
pb.set_message(state.draw(width));
}
let src = fs::read(file_path).with_context(|| {
format!("can not read `{}`", file_path.display())
})?;
let src = SourceCode::from(src.as_slice())
.with_origin(file_path.as_os_str().to_str().unwrap());
if args.get_flag("path-as-namespace") {
compiler
.new_namespace(file_path.to_string_lossy().as_ref());
}
let _ = compiler.add_source(src);
state.file_in_progress = None;
state.num_compiled_files =
state.num_compiled_files.saturating_add(1);
Ok(())
},
Err,
) {
if let Some(pb) = pb {
pb.finish_and_clear();
}
return Err(err);
}
}
if let Some(pb) = pb {
pb.finish_and_clear();
}
for warning in compiler.warnings() {
eprintln!("{warning}");
}
for error in compiler.errors() {
eprintln!("{error}");
}
if !compiler.errors().is_empty() {
bail!("{} error(s) found", compiler.errors().len());
}
let rules = compiler.build();
Ok(rules)
}
struct CompileState {
num_compiled_files: usize,
file_in_progress: Option<PathBuf>,
}
impl CompileState {
fn new() -> Self {
Self { num_compiled_files: 0, file_in_progress: None }
}
}
impl Draw for CompileState {
fn draw(&self, _width: usize) -> String {
if let Some(file) = &self.file_in_progress {
format!(
"{} {}...",
"Compiling".paint(Green).bold(),
file.display(),
)
} else {
String::new()
}
}
}
fn truncate_with_ellipsis(s: Cow<str>, max_width: usize) -> (Cow<str>, usize) {
let width = s.width();
if s.width() <= max_width {
return (s, width);
}
let mut with = 0;
let mut last_char_idx = 0;
for (idx, c) in s.char_indices() {
last_char_idx = idx;
let char_width = c.width().unwrap_or(0);
if with + char_width + 3 >= max_width {
break;
}
with += char_width;
}
(format!("{}...", &s[..last_char_idx]).into(), with + 3)
}