use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{fs, io, process};
use anyhow::Context;
use clap::{ArgAction, ArgMatches, Command, arg, value_parser};
use crossterm::tty::IsTty;
use yansi::Color::{Green, Red, Yellow};
use yansi::Paint;
use yara_x::{SourceCode, linters};
use yara_x_parser::ast::MetaValue;
use crate::config::{Config, MetaValueType};
use crate::walk::Draw;
use crate::walk::Message;
use crate::{help, walk};
pub fn check() -> Command {
super::command("check")
.about("Check if source files are syntactically correct")
.hide(true)
.long_about(help::CHECK_LONG_HELP)
.arg(
arg!(<RULES_PATH>)
.help("Path to YARA source file or directory")
.value_parser(value_parser!(PathBuf)),
)
.arg(
arg!(-f --filter <PATTERN>)
.help("Check files that match the given pattern only")
.long_help(help::FILTER_LONG_HELP)
.action(ArgAction::Append),
)
.arg(
arg!(-r - -"recursive"[MAX_DEPTH])
.help("Walk directories recursively up to a given depth")
.long_help(help::RECURSIVE_LONG_HELP)
.default_missing_value("1000")
.require_equals(true)
.value_parser(value_parser!(usize)),
)
.arg(
arg!(-p --"threads" <NUM_THREADS>)
.help("Use the given number of threads")
.long_help(help::THREADS_LONG_HELP)
.required(false)
.value_parser(value_parser!(u8).range(1..)),
)
}
fn is_sha256(s: &str) -> bool {
s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit())
}
fn is_sha1(s: &str) -> bool {
s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
}
fn is_md5(s: &str) -> bool {
s.len() == 32 && s.chars().all(|c| c.is_ascii_hexdigit())
}
pub fn exec_check(args: &ArgMatches, config: &Config) -> anyhow::Result<()> {
let rules_path = args.get_one::<PathBuf>("RULES_PATH").unwrap();
let recursive = args.get_one::<usize>("recursive");
let filters = args.get_many::<String>("filter");
let num_threads = args.get_one::<u8>("threads");
let mut w = walk::ParWalker::path(rules_path);
if let Some(num_threads) = num_threads {
w.num_threads(*num_threads);
}
if let Some(filters) = filters {
for filter in filters {
w.filter(filter);
}
} else {
w.filter("**/*.yar").filter("**/*.yara");
}
w.max_depth(*recursive.unwrap_or(&0));
let state = w.walk(
CheckState::new(),
|_, _| {},
|state, output, file_path, _| {
let src = fs::read(file_path.clone())
.with_context(|| {
format!("can not read `{}`", file_path.display())
})
.unwrap();
let src = SourceCode::from(src.as_slice())
.with_origin(file_path.as_os_str().to_str().unwrap());
let mut lines = Vec::new();
let mut compiler = yara_x::Compiler::new();
for (identifier, config) in config.check.metadata.iter() {
let mut linter =
linters::metadata(identifier)
.required(config.required)
.error(config.error);
match config.ty {
MetaValueType::String => {
let message = if let Some(regexp) = &config.regexp {
let _ = regex::bytes::Regex::new(regexp)?;
format!("`{identifier}` must be a string that matches `/{regexp}/`")
} else {
format!("`{identifier}` must be a string")
};
linter = linter.validator(
|meta| {
match (&meta.value, &config.regexp) {
(MetaValue::String((s, _)), Some(regexp)) => {
regex::Regex::new(regexp).unwrap().is_match(s)
}
(MetaValue::Bytes((s, _)), Some(regexp)) => {
regex::bytes::Regex::new(regexp).unwrap().is_match(s)
}
(MetaValue::String(_), None) => true,
(MetaValue::Bytes(_), None) => true,
_ => false,
}
},
message,
);
}
MetaValueType::Integer => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::Integer(_)),
format!("`{identifier}` must be an integer"),
);
}
MetaValueType::Float => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::Float(_)),
format!("`{identifier}` must be a float"),
);
}
MetaValueType::Bool => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::Bool(_)),
format!("`{identifier}` must be a bool"),
);
}
MetaValueType::Sha256 => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::String((s,_)) if is_sha256(s)),
format!("`{identifier}` must be a SHA-256"),
);
}
MetaValueType::Sha1 => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::String((s,_)) if is_sha1(s)),
format!("`{identifier}` must be a SHA-1"),
);
}
MetaValueType::MD5 => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::String((s,_)) if is_md5(s)),
format!("`{identifier}` must be a MD5"),
);
}
MetaValueType::Hash => {
linter = linter.validator(
|meta| matches!(meta.value, MetaValue::String((s,_))
if is_md5(s) || is_sha1(s) || is_sha256(s)),
format!("`{identifier}` must be a MD5, SHA-1 or SHA-256"),
);
}
}
compiler.add_linter(linter);
}
if let Some(re) = config
.check
.rule_name
.regexp
.as_ref()
.filter(|re| !re.is_empty()) {
compiler.add_linter(
linters::rule_name(re)?.error(config.check.rule_name.error));
}
if !config.check.tags.allowed.is_empty() {
compiler.add_linter(
linters::tags_allowed(config.check.tags.allowed.clone())
.error(config.check.tags.error));
} else if let Some(re) = config
.check
.tags
.regexp
.as_ref()
.filter(|re| !re.is_empty()) {
compiler.add_linter(
linters::tag_regex(re)?.error(config.check.tags.error)
);
}
compiler.colorize_errors(io::stdout().is_tty());
let _ = compiler.add_source(src);
if compiler.errors().is_empty() && compiler.warnings().is_empty() {
state.files_passed.fetch_add(1, Ordering::Relaxed);
lines.push(format!(
"[ {} ] {}",
"PASS".paint(Green).bold(),
file_path.display()
));
} else {
if !compiler.errors().is_empty() {
state.errors.fetch_add(
compiler.errors().len(),
Ordering::Relaxed,
);
lines.push(format!(
"[ {} ] {}",
"FAIL".paint(Red).bold(),
file_path.display()
));
for error in compiler.errors() {
eprintln!("{error}");
}
}
if !compiler.warnings().is_empty() {
state.warnings.fetch_add(
compiler.warnings().len(),
Ordering::Relaxed,
);
lines.push(format!(
"[ {} ] {}",
"WARN".paint(Yellow).bold(),
file_path.display()
));
for warning in compiler.warnings() {
eprintln!("{warning}");
}
}
}
output.send(Message::Info(lines.join("\n")))?;
Ok(())
},
|_, _| {},
|_| {},
|err, output| {
let _ = output.send(Message::Error(format!(
"{} {}",
"error:".paint(Red).bold(),
err
)));
Ok(())
},
)
.unwrap();
if state.errors.load(Ordering::Relaxed) > 0 {
process::exit(1)
}
if state.warnings.load(Ordering::Relaxed) > 0 {
process::exit(2);
}
Ok(())
}
#[derive(Debug)]
struct CheckState {
files_passed: AtomicUsize,
warnings: AtomicUsize,
errors: AtomicUsize,
}
impl CheckState {
fn new() -> Self {
Self {
files_passed: AtomicUsize::new(0),
warnings: AtomicUsize::new(0),
errors: AtomicUsize::new(0),
}
}
}
impl Draw for CheckState {
fn draw(&self, _width: usize) -> String {
let ok = format!(
"{} file(s) ok. ",
self.files_passed.load(Ordering::Relaxed)
);
let warnings =
format!("warnings: {}. ", self.warnings.load(Ordering::Relaxed));
let errors =
format!("errors: {}.", self.errors.load(Ordering::Relaxed));
format!(
"{}{}{}",
ok.paint(Green).bold(),
warnings.paint(Yellow).bold(),
errors.paint(Red).bold()
)
}
}