use std::fs;
use std::io::Write;
use std::path::Path;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::{NamedTempFile, TempDir};
fn cmd() -> Command {
Command::cargo_bin("boreal").unwrap()
}
fn test_file(contents: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(contents.as_bytes()).unwrap();
file
}
#[test]
fn test_no_arguments() {
cmd().assert().failure();
}
#[test]
fn test_invalid_path() {
cmd()
.arg("do_not_exist")
.arg("input")
.assert()
.stdout("")
.stderr(predicate::str::contains("IO error"))
.failure();
let rule_file = test_file("");
cmd()
.arg(rule_file.path())
.arg("bad_input")
.assert()
.stdout("")
.stderr(predicate::str::contains("Cannot scan bad_input"))
.failure();
}
#[test]
fn test_single_rule() {
let rule_file = test_file(
r#"
rule my_rule {
strings:
$a = "abc"
condition:
any of them
}"#,
);
let input = test_file("aaa");
cmd()
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout("")
.stderr("")
.success();
let input = test_file("zeabce");
cmd()
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout(predicate::eq(format!(
"my_rule {}\n",
input.path().display()
)))
.stderr("")
.success();
}
#[test]
fn test_rule_error() {
let rule_file = test_file(
r#"rule a {
strings: $a = /[z-a]/
condition: $a
}"#,
);
let input = test_file("");
cmd()
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout("")
.stderr(
predicate::str::contains("error")
.and(predicate::str::contains("invalid regex class range")),
)
.failure();
}
#[test]
fn test_rule_warning() {
let rule_file = test_file(
r#"rule rule_with_warning {
condition:
"a"
}
"#,
);
let input = test_file("");
cmd()
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout(predicate::eq(format!(
"rule_with_warning {}\n",
input.path().display()
)))
.stderr(
predicate::str::contains("warning").and(predicate::str::contains(
"implicit cast from a bytes value to a boolean",
)),
)
.success();
cmd()
.arg("--fail-on-warnings")
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout("")
.stderr(
predicate::str::contains("warning").and(predicate::str::contains(
"implicit cast from a bytes value to a boolean",
)),
)
.failure();
}
#[test]
fn test_rule_include() {
let temp = TempDir::new().unwrap();
let rule_b = temp.path().join("b.yar");
fs::write(
rule_b,
r#"
rule included {
strings:
$ = "abc"
condition:
any of them
}
"#,
)
.unwrap();
let rule_a = temp.path().join("a.yar");
fs::write(
&rule_a,
r#"
include "b.yar"
rule includer {
strings:
$ = "xyz"
condition:
included and any of them
}
"#,
)
.unwrap();
let input = test_file("abc");
cmd()
.arg(&rule_a)
.arg(input.path())
.assert()
.stdout(predicate::eq(format!(
"included {}\n",
input.path().display()
)))
.success();
let input = test_file("xyz abc");
cmd()
.arg(rule_a)
.arg(input.path())
.assert()
.stdout(
predicate::str::contains(format!("included {}", input.path().display())).and(
predicate::str::contains(format!("includer {}", input.path().display())),
),
)
.success();
}
#[test]
fn test_rule_include_error() {
let temp = TempDir::new().unwrap();
let rule_b = temp.path().join("b.yar");
fs::write(
&rule_b,
r#"
rule included {
strings:
$ = /[z-a]/
condition:
any of them
}
"#,
)
.unwrap();
let rule_a = temp.path().join("a.yar");
fs::write(&rule_a, r#"include "b.yar""#).unwrap();
let input = test_file("");
cmd()
.arg(rule_a)
.arg(input.path())
.assert()
.stdout("")
.stderr(
predicate::str::contains(rule_b.canonicalize().unwrap().display().to_string())
.and(
predicate::str::contains("z-a"),
)
.and(
predicate::str::contains("invalid regex class range"),
),
)
.failure();
}
#[test]
#[cfg(unix)]
fn test_rule_dir() {
use std::os::unix::fs::symlink;
let rule_file = test_file("rule a { condition: true }");
let temp = TempDir::new().unwrap();
let temp2 = TempDir::new().unwrap();
let file_a = temp.path().join("a");
fs::write(&file_a, "").unwrap();
let file_b = temp.path().join("b");
fs::write(&file_b, "").unwrap();
let subdir = temp.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let file_c = subdir.join("c");
fs::write(&file_c, "").unwrap();
let file2_d = temp2.path().join("d");
fs::write(&file2_d, "").unwrap();
let file2_e = temp2.path().join("e");
fs::write(&file2_e, "").unwrap();
let file_d = temp.path().join("d");
symlink(&file2_d, &file_d).unwrap();
let file_e = subdir.join("e");
symlink(&file2_e, &file_e).unwrap();
let match_str = |input: &Path| predicate::str::contains(format!("a {}", input.display()));
cmd()
.args(["--threads", "20"])
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b))
.and(match_str(&file_c).not())
.and(match_str(&file_d))
.and(match_str(&file_e).not()),
)
.stderr("")
.success();
cmd()
.arg("-N")
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b))
.and(match_str(&file_c).not())
.and(match_str(&file_d).not())
.and(match_str(&file_e).not()),
)
.stderr("")
.success();
cmd()
.arg("-r")
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b))
.and(match_str(&file_c))
.and(match_str(&file_d))
.and(match_str(&file_e)),
)
.stderr("")
.success();
cmd()
.arg("--recursive")
.arg("--no-follow-symlinks")
.args(["-p", "2"])
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b))
.and(match_str(&file_c))
.and(match_str(&file_d).not())
.and(match_str(&file_e).not()),
)
.stderr("")
.success();
}
#[test]
fn test_skip_larger() {
let rule_file = test_file("rule a { condition: true }");
let temp = TempDir::new().unwrap();
let file_a = temp.path().join("a");
fs::write(&file_a, "").unwrap();
let file_b = temp.path().join("b");
fs::write(&file_b, [0; 100]).unwrap();
let file_c = temp.path().join("c");
fs::write(&file_c, [0; 1024]).unwrap();
let match_file = |input: &Path| predicate::str::contains(format!("a {}", input.display()));
let skip_file =
|input: &Path| predicate::str::contains(format!("skipping {}", input.display()));
cmd()
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_file(&file_a)
.and(match_file(&file_b))
.and(match_file(&file_c)),
)
.stderr("")
.success();
cmd()
.args(["--skip-larger", "512"])
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_file(&file_a)
.and(match_file(&file_b))
.and(match_file(&file_c).not()),
)
.stderr(
skip_file(&file_a)
.not()
.and(skip_file(&file_b).not())
.and(skip_file(&file_c)),
)
.success();
cmd()
.args(["-z", "10"])
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout(
match_file(&file_a)
.and(match_file(&file_b).not())
.and(match_file(&file_c).not()),
)
.stderr(
skip_file(&file_a)
.not()
.and(skip_file(&file_b))
.and(skip_file(&file_c)),
)
.success();
}
#[test]
fn test_print_module_data() {
let rule_file = test_file(
r#"
import "pe"
rule a {
condition: false
}
"#,
);
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("boreal")
.join("tests")
.join("assets")
.join("libyara")
.join("data")
.join("mtxex.dll");
cmd()
.arg("-D")
.arg(rule_file.path())
.arg(input)
.assert()
.stdout(
predicate::str::starts_with("pe\n")
.and(predicate::str::contains("base_of_code = 4096 (0x1000)"))
.and(predicate::str::contains("base_of_data[undef]"))
.and(predicate::str::contains(
r#"
data_directories
[0]
size = 220 (0xdc)
"#,
))
.and(predicate::str::contains("delayed_import_details = []"))
.and(predicate::str::contains(r#"dll_name = "mtxex.dll""#))
.and(predicate::str::contains(
"[\"ProductName\"] = { 4d6963726f736f6674ae2057696e646f7773ae204f7065\
726174696e672053797374656d }",
))
.and(predicate::str::contains(
r#"
image_version
major = 10 (0xa)
minor = 0 (0x0)
"#,
))
.and(predicate::str::contains(
r#"
version_info
["CompanyName"] = "Microsoft Corporation"
"#,
)),
)
.stderr("")
.success();
}
#[test]
fn test_print_string_stats() {
let rule_file = test_file(
r#"
rule a {
strings:
$a = "ab<de>g"
$b = { 01 ( FE | EF ) }
$c = /foo\d??barbaz/ fullword
$d = /.{10}/ fullword
condition:
any of them
}
"#,
);
let stats = r#"
$a = "ab<de>g"
literals: ["ab<de>g"]
atoms: ["<de>"]
atoms quality: 84
algo: Literals
$b = { 01 ( FE | EF ) }
literals: [{ 01fe }, { 01ef }]
atoms: [{ 01fe }, { 01ef }]
atoms quality: 44
algo: Literals
$c = /foo\d??barbaz/ fullword
literals: ["barbaz"]
atoms: ["rbaz"]
atoms quality: 80
algo: Atomized { NonGreedy { reverse: Dfa, forward: none } }
$d = /.{10}/ fullword
literals: []
atoms: []
atoms quality: 0
algo: Raw
"#;
let input = test_file("");
cmd()
.arg("--string-stats")
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout(predicate::eq(format!(
"default:a (from {}){}",
rule_file.path().display(),
stats
)))
.stderr("")
.success();
}
#[test]
fn test_print_scan_stats() {
let rule_file = test_file(
r#"
rule a {
strings:
$a = "abc"
condition:
any of them
}
"#,
);
let input = test_file("abc");
cmd()
.arg("--scan-stats")
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout(
predicate::str::is_match(
r"Evaluation \{
no_scan_eval_duration: .*,
ac_duration: .*,
ac_confirm_duration: .*,
nb_ac_matches: .*,
rules_eval_duration: .*,
raw_regexes_eval_duration: .*,
\}
",
)
.unwrap(),
)
.stderr("")
.success();
}
#[test]
#[cfg(unix)]
fn test_input_cannot_read() {
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let rule_file = test_file("rule bee { condition: true }");
let temp = TempDir::new().unwrap();
let child = temp.path().join("child");
let _file = fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o000)
.open(&child)
.unwrap();
let subdir = temp.path().join("subdir");
fs::create_dir(&subdir).unwrap();
fs::set_permissions(&subdir, fs::Permissions::from_mode(0o000)).unwrap();
cmd()
.arg("-r")
.arg(rule_file.path())
.arg(temp.path())
.assert()
.stdout("")
.stderr(
predicate::str::contains(format!("Cannot scan file {}", child.display())).and(
predicate::str::contains(format!("IO error for operation on {}", subdir.display())),
),
)
.success();
}
#[test]
fn test_module_names() {
cmd()
.arg("-M")
.assert()
.stdout(
predicate::str::contains("math\n")
.and(predicate::str::contains("string\n"))
.and(predicate::str::contains("time\n")),
)
.stderr("")
.success();
}