#![allow(missing_docs)]
#![allow(unused_results)]
#![allow(unused_crate_dependencies)]
#![allow(clippy::pedantic)]
use std::io::{BufRead, Write};
use std::path::Path;
use std::{fs, io::BufReader};
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::{NamedTempFile, TempDir};
fn test_file(contents: &[u8]) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(contents).unwrap();
file
}
enum CmdKind {
#[cfg(feature = "serialize")]
Save,
#[cfg(feature = "serialize")]
Load,
ListModules,
}
fn boreal_cmd() -> Command {
Command::cargo_bin("boreal").unwrap()
}
fn test_scan<F>(options: &[&str], rules_files: &[&Path], input: &Path, test: F)
where
F: Fn(&mut Command),
{
let mut cmd = boreal_cmd();
cmd.arg("scan");
for opt in options {
cmd.arg(opt);
}
for file in rules_files {
cmd.arg("-f").arg(file);
}
cmd.arg(input);
test(&mut cmd);
let mut cmd = boreal_cmd();
cmd.arg("yr");
for opt in options {
cmd.arg(opt);
}
for file in rules_files {
cmd.arg(file);
}
cmd.arg(input);
test(&mut cmd);
}
fn test_cmd<F>(kind: CmdKind, test: F)
where
F: Fn(&mut Command),
{
match kind {
#[cfg(feature = "serialize")]
CmdKind::Save => {
test(boreal_cmd().arg("save"));
}
#[cfg(feature = "serialize")]
CmdKind::Load => {
test(boreal_cmd().arg("yr").arg("-C"));
test(boreal_cmd().arg("load"));
}
CmdKind::ListModules => {
test(boreal_cmd().arg("yr").arg("-M"));
test(boreal_cmd().arg("list-modules"));
}
}
}
#[test]
fn test_no_arguments() {
boreal_cmd().arg("yr").assert().failure();
boreal_cmd().arg("scan").assert().failure();
}
#[test]
fn test_invalid_path() {
test_scan(
&[],
&[Path::new("do_not_exist")],
Path::new("input"),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"Cannot read rules file do_not_exist: ",
))
.failure();
},
);
let rule_file = test_file(b"");
test_scan(&[], &[rule_file.path()], Path::new("bad_input"), |cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains("Cannot scan bad_input"))
.failure();
});
}
#[test]
fn test_scan_file() {
let rule_file = test_file(
br#"
rule my_rule {
strings:
$a = "abc"
condition:
any of them
}"#,
);
let input = test_file(b"aaa");
test_scan(
&["--profile", "memory"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert().stdout("").stderr("").success();
},
);
let input = test_file(b"zeabce");
test_scan(&[], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("my_rule {}\n", input.path().display()))
.stderr("")
.success();
});
}
#[test]
#[cfg_attr(unix, ignore)]
fn test_scan_process() {
let rule_file = test_file(
br#"
rule process_scan {
strings:
$a = "PAYLOAD_ON_STACK"
condition:
$a
}"#,
);
let proc = BinHelper::run("stack");
let pid = proc.pid();
test_scan(
&[],
&[rule_file.path()],
Path::new(&pid.to_string()),
|cmd| {
cmd.assert()
.stdout(format!("process_scan {}\n", pid))
.stderr("")
.success();
},
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_scan_process_not_found() {
let rule_file = test_file(b"rule process_scan { condition: true }");
let mut pid = 999_999_999;
while Path::new("proc").join(pid.to_string()).exists() {
pid += 1;
}
test_scan(
&[],
&[rule_file.path()],
Path::new(&pid.to_string()),
|cmd| {
cmd.assert()
.stdout("")
.stderr(format!("Cannot scan {}: unknown process\n", pid))
.failure();
},
);
}
#[test]
fn test_scan_file_with_process_name() {
let rule_file = test_file(
br#"
rule is_file {
strings:
$a = "buzo"
condition:
any of them
}"#,
);
let temp = TempDir::new().unwrap();
let file = temp.path().join("1");
fs::write(file, "gabuzomeu").unwrap();
test_scan(&[], &[rule_file.path()], Path::new("1"), |cmd| {
cmd.current_dir(temp.path())
.assert()
.stdout("is_file 1\n")
.stderr("")
.success();
});
}
#[test]
fn test_rule_error() {
let rule_file = test_file(
br#"rule a {
strings: $a = /[z-a]/
condition: $a
}"#,
);
let input = test_file(b"");
test_scan(&[], &[rule_file.path()], input.path(), |cmd| {
cmd.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(
br#"rule rule_with_warning {
condition:
"a"
}
"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&[], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("rule_with_warning {path}\n"))
.stderr(
predicate::str::contains("warning").and(predicate::str::contains(
"implicit cast from a bytes value to a boolean",
)),
)
.success();
});
test_scan(
&["--fail-on-warnings"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout("")
.stderr(
predicate::str::contains("warning").and(predicate::str::contains(
"implicit cast from a bytes value to a boolean",
)),
)
.failure();
},
);
test_scan(&["-w"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("rule_with_warning {path}\n"))
.stderr("")
.success();
});
}
#[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(b"abc");
test_scan(&[], &[&rule_a], input.path(), |cmd| {
cmd.assert()
.stdout(format!("included {}\n", input.path().display()))
.success();
});
let input = test_file(b"xyz abc");
test_scan(&[], &[&rule_a], input.path(), |cmd| {
cmd.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(b"");
test_scan(&[], &[&rule_a], input.path(), |cmd| {
cmd.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(b"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()));
test_scan(
&["--threads", "20"],
&[rule_file.path()],
temp.path(),
|cmd| {
cmd.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();
},
);
test_scan(&["-N"], &[rule_file.path()], temp.path(), |cmd| {
cmd.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();
});
test_scan(&["-r"], &[rule_file.path()], temp.path(), |cmd| {
cmd.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();
});
test_scan(
&["--recursive", "--no-follow-symlinks", "-p", "2"],
&[rule_file.path()],
temp.path(),
|cmd| {
cmd.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(b"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()));
test_scan(&[], &[rule_file.path()], temp.path(), |cmd| {
cmd.assert()
.stdout(
match_file(&file_a)
.and(match_file(&file_b))
.and(match_file(&file_c)),
)
.stderr("")
.success();
});
test_scan(
&["--skip-larger", "512"],
&[rule_file.path()],
temp.path(),
|cmd| {
cmd.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();
},
);
test_scan(&["-z", "10"], &[rule_file.path()], temp.path(), |cmd| {
cmd.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(
br#"
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");
test_scan(&["-D"], &[rule_file.path()], &input, |cmd| {
cmd.assert()
.stdout(
predicate::str::starts_with("pe\n")
.and(predicate::str::contains("DLL = 8192 (0x2000)"))
.and(predicate::str::contains("section_index[function]"))
.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(
br#"
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(b"");
test_scan(
&["--string-stats"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!(
"default:a (from {}){}",
rule_file.path().display(),
stats
))
.stderr("")
.success();
},
);
}
#[test]
fn test_print_scan_stats() {
let rule_file = test_file(
br#"
rule a {
strings:
$a = "abc"
condition:
any of them
}
"#,
);
let input = test_file(b"abc");
test_scan(
&["--scan-stats"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(
predicate::str::is_match(
r"Evaluation \{
no_scan_eval_duration: .*,
ac_duration: .*,
fetch_memory_duration: .*,
ac_confirm_duration: .*,
nb_ac_matches: .*,
rules_eval_duration: .*,
raw_regexes_eval_duration: .*,
memory_scanned_size: .*,
nb_memory_chunks: .*,
\}
",
)
.unwrap(),
)
.stderr("")
.success();
},
);
}
#[test]
#[cfg(unix)]
fn test_input_cannot_read() {
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let rule_file = test_file(b"rule bee { condition: true }");
let temp = TempDir::new().unwrap();
let child = temp.path().join("child");
let _file = fs::OpenOptions::new()
.create(true)
.truncate(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();
test_scan(&["-r"], &[rule_file.path()], temp.path(), |cmd| {
cmd.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() {
test_cmd(CmdKind::ListModules, |cmd| {
cmd.assert()
.stdout(
predicate::str::contains("math\n")
.and(predicate::str::contains("string\n"))
.and(predicate::str::contains("time\n")),
)
.stderr("")
.success();
});
}
#[test]
#[cfg(feature = "memmap")]
fn test_no_mmap() {
let rule_file = test_file(
br#"
rule first {
strings:
$a = "abc"
condition:
any of them
}
rule second {
strings:
$a = "xyz"
condition:
any of them
}"#,
);
let input = test_file(b"xyabcz");
test_scan(&["--no-mmap"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("first {}\n", input.path().display()))
.stderr("")
.success();
});
}
#[test]
fn test_console_log() {
let rule_file = test_file(
br#"
import "console"
rule logger {
condition:
console.log("this is ", "a log")
}"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&[], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("this is a log\nlogger {path}\n"))
.stderr("")
.success();
});
test_scan(&["-q"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("logger {path}\n"))
.stderr("")
.success();
});
}
#[test]
fn test_invalid_fragmented_scan_mode() {
test_scan(
&["--fragmented-scan-mode", "bad_value"],
&[Path::new("rules.yar")],
Path::new("input"),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"invalid value 'bad_value' for \
'--fragmented-scan-mode <legacy|fast|singlepass>\': invalid value",
))
.failure();
},
);
}
#[test]
fn test_invalid_compiler_profile() {
test_scan(
&["--profile", "bad_value"],
&[Path::new("rules.yar")],
Path::new("input"),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"invalid value 'bad_value' for \
'--profile <speed|memory>\': invalid value",
))
.failure();
},
);
}
#[test]
fn test_tags() {
let rule_file = test_file(
br#"
rule notag {
condition:
true
}
rule tag1: first {
condition:
true
}
rule tag3: first second third {
condition:
true
}
"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&["-g"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
"notag [] {path}\n\
tag1 [first] {path}\n\
tag3 [first,second,third] {path}\n"
))
.stderr("")
.success();
});
test_scan(&["-t", "first"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("tag1 {path}\ntag3 {path}\n"))
.stderr("")
.success();
});
test_scan(&["--tag=third"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("tag3 {path}\n"))
.stderr("")
.success();
});
test_scan(&["-t", ""], &[rule_file.path()], input.path(), |cmd| {
cmd.assert().stdout("").stderr("").success();
});
}
#[test]
fn test_identifier() {
let rule_file = test_file(
br#"
rule first { condition: true }
rule second { condition: true }
"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&["-i", "first"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("first {path}\n"))
.stderr("")
.success();
});
test_scan(
&["--identifier=second"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!("second {path}\n"))
.stderr("")
.success();
},
);
test_scan(
&["--identifier=third"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert().stdout("").stderr("").success();
},
);
}
#[test]
fn test_print_meta() {
let rule_file = test_file(
br#"
rule first: tag {
meta:
integer = -15
string = "d mol"
test = true
condition:
true
}
rule second: tag {
condition:
true
}
rule third: tag {
meta:
value = "ok"
condition:
true
}
rule fourth { condition: true }
"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&["-m"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
"first [integer=-15,string=\"d mol\",test=true] {path}\n\
second [] {path}\n\
third [value=\"ok\"] {path}\n\
fourth [] {path}\n"
))
.stderr("")
.success();
});
test_scan(
&["-g", "--print-meta"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!(
"first [tag] [integer=-15,string=\"d mol\",test=true] {path}\n\
second [tag] [] {path}\n\
third [tag] [value=\"ok\"] {path}\n\
fourth [] [] {path}\n"
))
.stderr("")
.success();
},
);
}
#[test]
fn test_print_string_matches() {
let rule_file = test_file(
br#"
rule my_rule {
strings:
$a = /<.{1,5}?>/
$b = "abc"
condition:
// The rule can be evaluated without scanning the strings.
// This ensures that printing strings forces the computation of the
// string matches.
true or any of them
}"#,
);
let input = test_file(
b"<a>\n
<<abc>d>\n
<\x01\x02>\n
<a\tv>\n
<a\xFF>>\n
",
);
let path = input.path().display();
test_scan(&["-s"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
r#"my_rule {path}
0x0:$a: <a>
0x5:$a: <<abc>
0x6:$a: <abc>
0xf:$a: <\x01\x02>
0x15:$a: <a\tv>
0x1c:$a: <a\xff>
0x7:$b: abc
"#
))
.stderr("")
.success();
});
test_scan(&["-L"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
r#"my_rule {path}
0x0:3:$a
0x5:6:$a
0x6:5:$a
0xf:4:$a
0x15:5:$a
0x1c:4:$a
0x7:3:$b
"#
))
.stderr("")
.success();
});
test_scan(
&["--print-strings", "--print-string-length"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!(
r#"my_rule {path}
0x0:3:$a: <a>
0x5:6:$a: <<abc>
0x6:5:$a: <abc>
0xf:4:$a: <\x01\x02>
0x15:5:$a: <a\tv>
0x1c:4:$a: <a\xff>
0x7:3:$b: abc
"#
))
.stderr("")
.success();
},
);
}
#[test]
fn test_print_string_xor_key() {
let rule_file = test_file(
br#"
rule my_rule {
strings:
$a = "aaa" ascii wide xor
condition:
// The rule can be evaluated without scanning the strings.
// This ensures that printing xor keys forces the computation of the
// string matches.
true or any of them
}"#,
);
let input = test_file(b"aaa\nbbb\nabcccba\nB#B#B#\n");
let path = input.path().display();
test_scan(&["-X"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
r#"my_rule {path}
0x0:$a:xor(0x00,aaa)
0x4:$a:xor(0x03,aaa)
0xa:$a:xor(0x02,aaa)
0x10:$a:xor(0x23,a\x00a\x00a\x00)
"#
))
.stderr("")
.success();
});
test_scan(&["-Xs"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
r#"my_rule {path}
0x0:$a:xor(0x00,aaa): aaa
0x4:$a:xor(0x03,aaa): bbb
0xa:$a:xor(0x02,aaa): ccc
0x10:$a:xor(0x23,a\x00a\x00a\x00): B#B#B#
"#
))
.stderr("")
.success();
});
}
#[test]
fn test_timeout() {
let rule_file = test_file(
br#"
rule too_long {
condition:
for all i in (0..9223372036854775807) : (
for all j in (0..9223372036854775807) : (
for all k in (0..9223372036854775807) : (
for all l in (0..9223372036854775807) : (
i + j + k + l >= 0
)
)
)
)
}"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&["-a", "1"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout("")
.stderr(format!("Cannot scan {path}: timeout\n"))
.failure();
});
}
#[test]
fn test_print_namespace() {
let rule_file = test_file(
br#"
rule first { condition: true }
"#,
);
let input = test_file(b"");
let path = input.path().display();
test_scan(&["-e"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("default:first {path}\n"))
.stderr("")
.success();
});
}
#[test]
fn test_define_symbol() {
let input = test_file(b"");
let path = input.path().display();
let rule_file = test_file(
br#"
rule symbols {
condition:
symbol_float + 4.3 == 2.5 and
symbol_int + 5 == 2 and
symbol_true == true and
symbol_false == false and
symbol_str == "a_string" and
symbol_str2 == "2.5a" and
symbol_empty == ""
}
"#,
);
test_scan(
&[
"-d",
"symbol_float=-1.8",
"--define=symbol_int=-3",
"--define",
"symbol_true=true",
"--define",
"symbol_false=false",
"--define",
"symbol_str=a_string",
"--define",
"symbol_str2=2.5a",
"--define",
"symbol_empty=",
],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!("symbols {path}\n"))
.stderr("")
.success();
},
);
test_scan(&["-d", "name"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"invalid value 'name' for '--define <VAR=VALUE>': \
missing '=' delimiter",
))
.failure();
});
let bad_rule = test_file(
br#"
rule bad_symbols {
condition:
symbol_float == "-1.8"
}
"#,
);
test_scan(
&["-d", "symbol_float=-1.8"],
&[bad_rule.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains("expressions have invalid types"))
.failure();
},
);
}
#[test]
fn test_scan_list() {
let rule_file = test_file(b"rule a { condition: true }");
let dir1 = TempDir::new().unwrap();
let file_a = dir1.path().join("a");
fs::write(&file_a, "").unwrap();
let file_b = dir1.path().join("b");
fs::write(&file_b, "").unwrap();
let dir2 = TempDir::new().unwrap();
let subdir = dir2.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let file_c = subdir.join("c");
fs::write(&file_c, "").unwrap();
let match_str = |input: &Path| predicate::str::contains(format!("a {}", input.display()));
let list = test_file(format!("{}\n{}\n", dir1.path().display(), file_c.display()).as_bytes());
test_scan(&["--scan-list"], &[rule_file.path()], list.path(), |cmd| {
cmd.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b))
.and(match_str(&file_c)),
)
.stderr("")
.success();
});
let list = test_file(format!("{}\n{}\n", file_a.display(), dir2.path().display()).as_bytes());
test_scan(&["--scan-list"], &[rule_file.path()], list.path(), |cmd| {
cmd.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b).not())
.and(match_str(&file_c).not()),
)
.stderr("")
.success();
});
test_scan(
&["--scan-list", "-r"],
&[rule_file.path()],
list.path(),
|cmd| {
cmd.assert()
.stdout(
match_str(&file_a)
.and(match_str(&file_b).not())
.and(match_str(&file_c)),
)
.stderr("")
.success();
},
);
let list = test_file(b"");
test_scan(&["--scan-list"], &[rule_file.path()], list.path(), |cmd| {
cmd.assert().stdout("").stderr("").success();
});
if cfg!(target_os = "linux") {
test_scan(&["--scan-list"], &[rule_file.path()], dir1.path(), |cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(format!(
"cannot read from scan list {}: Is a directory",
dir1.path().display()
)))
.failure();
});
test_scan(
&["--scan-list"],
&[rule_file.path()],
Path::new("invalid_path"),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"cannot open scan list invalid_path: No such file or directory",
))
.failure();
},
);
}
}
#[test]
fn test_negate() {
let rule_file = test_file(
br#"
rule a { condition: true }
rule b { condition: false }
rule c { condition: true }
rule d { condition: false }
"#,
);
let input = test_file(b"");
test_scan(&["-n"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
"b {}\nd {}\n",
input.path().display(),
input.path().display()
))
.stderr("")
.success();
});
}
#[test]
fn test_count() {
let rule_file = test_file(
br#"
rule a {
strings:
$ = "abc"
condition:
any of them
}
rule b {
strings:
$ = "def"
condition:
any of them
}
"#,
);
let temp = TempDir::new().unwrap();
let file_a = temp.path().join("a");
fs::write(&file_a, "abc").unwrap();
let file_b = temp.path().join("b");
fs::write(&file_b, "abcdef").unwrap();
let file_c = temp.path().join("c");
fs::write(&file_c, "").unwrap();
test_scan(&["-c"], &[rule_file.path()], &file_a, |cmd| {
cmd.assert()
.stdout(format!("{}: 1\n", file_a.display()))
.stderr("")
.success();
});
test_scan(&["-cn"], &[rule_file.path()], &file_c, |cmd| {
cmd.assert()
.stdout(format!("{}: 2\n", file_c.display()))
.stderr("")
.success();
});
test_scan(&["-c"], &[rule_file.path()], temp.path(), |cmd| {
cmd.assert()
.stdout(
predicate::str::contains(format!("{}: 1", file_a.display()))
.and(predicate::str::contains(format!("{}: 2", file_b.display())))
.and(predicate::str::contains(format!("{}: 0", file_c.display()))),
)
.stderr("")
.success();
});
}
#[test]
#[cfg_attr(unix, ignore)]
fn test_count_process() {
let rule_file = test_file(
br#"
rule process_scan {
strings:
$a = "PAYLOAD_ON_STACK"
condition:
$a
}"#,
);
let proc = BinHelper::run("stack");
let pid = proc.pid();
test_scan(
&["--count"],
&[rule_file.path()],
Path::new(&pid.to_string()),
|cmd| {
cmd.assert()
.stdout(format!("{pid}: 1\n"))
.stderr("")
.success();
},
);
}
#[test]
#[cfg_attr(unix, ignore)]
fn test_count_limit() {
let rule_file = test_file(
br#"
rule a { condition: true }
rule b { condition: true }
rule c { condition: true }
"#,
);
let input = test_file(b"");
test_scan(&["-l", "2"], &[rule_file.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!(
"a {}\nb {}\n",
input.path().display(),
input.path().display(),
))
.stderr("")
.success();
});
let proc = BinHelper::run("stack");
let pid = proc.pid();
test_scan(
&["--max-rules=2"],
&[rule_file.path()],
Path::new(&pid.to_string()),
|cmd| {
cmd.assert()
.stdout(format!("a {pid}\nb {pid}\n"))
.stderr("")
.success();
},
);
}
#[test]
fn test_max_strings_per_rule() {
let rule_file = test_file(
br#"
rule a {
strings:
$a = "aaa"
$b = "bbb"
$c = "ccc"
condition:
any of them
}
"#,
);
let input = test_file(b"");
test_scan(
&["--max-strings-per-rule=2"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout("")
.stderr(
predicate::str::contains("error").and(predicate::str::contains(
"the rule contains more than 2 strings",
)),
)
.failure();
},
);
}
#[test]
fn test_string_max_nb_matches() {
let rule_file = test_file(
br#"
rule a {
strings:
$a = "a"
condition:
any of them
}
"#,
);
let warning = "warning: string $a in rule default:a reached the maximum number of matches";
let input = test_file(b"aaaa");
test_scan(
&["--string-max-nb-matches=2"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!("a {}\n", input.path().display()))
.stderr(predicate::str::contains(warning))
.success();
},
);
test_scan(
&["--string-max-nb-matches=2", "--no-warnings"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!("a {}\n", input.path().display()))
.stderr("")
.success();
},
);
test_scan(
&["--string-max-nb-matches=2", "--fail-on-warnings"],
&[rule_file.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(warning))
.success();
},
);
}
#[test]
fn test_yr_invalid_number_of_arguments() {
boreal_cmd()
.arg("yr")
.arg("rules_file")
.assert()
.stderr(predicate::str::contains("Invalid number of arguments"))
.failure();
#[cfg(feature = "serialize")]
{
boreal_cmd()
.arg("yr")
.arg("-C")
.arg("rules_file")
.arg("rules_file2")
.arg("input")
.assert()
.stderr(predicate::str::contains(
"Only a single rules path must be passed",
))
.failure();
}
}
#[test]
fn test_multiple_rules_files() {
let rule_a = test_file(b"rule a { condition: true }");
let rule_b = test_file(b"rule b { condition: true }");
let rule_c = test_file(b"rule c { condition: true }");
let input = test_file(b"");
let input_path = input.path().display().to_string();
test_scan(&[], &[rule_a.path(), rule_b.path()], input.path(), |cmd| {
cmd.assert()
.stdout(format!("a {input_path}\nb {input_path}\n"))
.success();
});
test_scan(
&[],
&[rule_a.path(), rule_b.path(), rule_c.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout(format!("a {input_path}\nb {input_path}\nc {input_path}\n"))
.success();
},
);
test_scan(
&[],
&[rule_a.path(), rule_b.path(), rule_a.path()],
input.path(),
|cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"rule `a` is already declared in this namespace",
))
.failure();
},
);
}
#[test]
fn test_rules_files_namespace() {
let temp = TempDir::new().unwrap();
let rule_a = temp.path().join("rule_a");
fs::write(&rule_a, b"rule a { condition: true }").unwrap();
let rule_b = temp.path().join("rule:b");
fs::write(&rule_b, b"rule b { condition: true }").unwrap();
let input = temp.path().join("input");
fs::write(&input, b"").unwrap();
let input_path = input.display().to_string();
test_scan(
&["-e"],
&[Path::new("ns1:rule_a"), Path::new("ns2:rule_a")],
&input,
|cmd| {
cmd.current_dir(temp.path())
.assert()
.stdout(format!("ns1:a {input_path}\nns2:a {input_path}\n"))
.success();
},
);
test_scan(
&["-e"],
&[
Path::new("ns1:rule_a"),
Path::new("rule:b"),
Path::new("ns2:rule:b"),
],
&input,
|cmd| {
cmd.current_dir(temp.path())
.assert()
.stdout(format!(
"ns1:a {input_path}\ndefault:b {input_path}\nns2:b {input_path}\n"
))
.success();
},
);
test_scan(&["-e"], &[Path::new("rule:c")], &input, |cmd| {
cmd.current_dir(temp.path())
.assert()
.stdout("")
.stderr(predicate::str::contains("Cannot read rules file c:"))
.failure();
});
test_scan(&["-e"], &[Path::new("ns1:rule:c")], &input, |cmd| {
cmd.current_dir(temp.path())
.assert()
.stdout("")
.stderr(predicate::str::contains("Cannot read rules file rule:c:"))
.failure();
});
}
#[test]
fn test_ignored_arguments() {
let rule_file = test_file(b"rule a { strings: $ = /abc/ condition: any of them }");
let input = test_file(b"abc");
boreal_cmd()
.arg("yr")
.arg("-E")
.arg("--fast-scan")
.arg("--stack-size=300")
.arg(rule_file.path())
.arg(input.path())
.assert()
.stdout(format!("a {}\n", input.path().display()))
.stderr("")
.success();
}
#[test]
#[cfg(feature = "serialize")]
fn test_save_load() {
let rule_file = test_file(
br#"
import "console"
rule a {
condition:
console.log("foo")
}
"#,
);
let temp = TempDir::new().unwrap();
let save_path = temp.path().join("serialized_rules");
let input = temp.path().join("input");
fs::write(&input, b"a").unwrap();
test_cmd(CmdKind::Save, |cmd| {
cmd.arg("-f")
.arg(rule_file.path())
.arg(&save_path)
.assert()
.success();
});
test_cmd(CmdKind::Load, |cmd| {
cmd.arg(&save_path)
.arg(&input)
.assert()
.stdout(format!("foo\na {}\n", input.display()))
.stderr("")
.success();
});
}
#[test]
#[cfg(feature = "serialize")]
fn test_invalid_load() {
let temp = TempDir::new().unwrap();
let input = temp.path().join("input");
fs::write(&input, b"a").unwrap();
let fake_path = temp.path().join("non_existing");
test_cmd(CmdKind::Load, |cmd| {
cmd.arg(&fake_path)
.arg(&input)
.assert()
.stdout("")
.stderr(predicate::str::contains(format!(
"Unable to read from {}",
fake_path.display()
)))
.failure();
});
test_cmd(CmdKind::Load, |cmd| {
cmd.arg(&input)
.arg(&input)
.assert()
.stdout("")
.stderr(predicate::str::contains("Unable to deserialize rules"))
.failure();
});
}
#[test]
#[cfg(feature = "serialize")]
fn test_invalid_save() {
let temp = TempDir::new().unwrap();
let rule = temp.path().join("rule");
fs::write(&rule, b"rule a { condition: true }").unwrap();
test_cmd(CmdKind::Save, |cmd| {
cmd.arg("-f")
.arg(&rule)
.arg(&rule)
.assert()
.stdout("")
.stderr(predicate::str::contains(format!(
"File {} already exists",
rule.display()
)))
.failure();
});
let bad_rule = temp.path().join("bad_rule");
fs::write(&bad_rule, b"rule a { ").unwrap();
test_cmd(CmdKind::Save, |cmd| {
cmd.arg("-f")
.arg(&bad_rule)
.arg(&bad_rule)
.assert()
.stdout("")
.stderr(predicate::str::contains("syntax error"))
.failure();
});
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let subdir = temp.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let out = subdir.join("out");
let mut perms = fs::metadata(&subdir).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&subdir, perms).unwrap();
test_cmd(CmdKind::Save, |cmd| {
cmd.arg("-f")
.arg(&rule)
.arg(&out)
.assert()
.stdout("")
.stderr(predicate::str::contains(format!(
"Unable to inspect file {}",
out.display()
)))
.failure();
});
let mut perms = fs::metadata(&subdir).unwrap().permissions();
perms.set_mode(0o555);
fs::set_permissions(&subdir, perms).unwrap();
let out_path = subdir.join("out");
test_cmd(CmdKind::Save, |cmd| {
cmd.arg("-f")
.arg(&rule)
.arg(&out_path)
.assert()
.stdout("")
.stderr(predicate::str::contains(format!(
"Unable to create file {}",
out.display()
)))
.failure();
});
}
}
#[test]
#[cfg(feature = "cuckoo")]
fn test_module_data() {
let temp = TempDir::new().unwrap();
let rule = temp.path().join("rule");
fs::write(
&rule,
r#"
import "cuckoo"
rule a {
condition:
cuckoo.network.host(/bcd/) == 1
}"#,
)
.unwrap();
let data = temp.path().join("data");
fs::write(&data, r#"{ "network": { "hosts": ["abcde"] } }"#).unwrap();
let input = temp.path().join("input");
fs::write(&input, "").unwrap();
test_scan(
&[&format!("--module-data=cuckoo={}", data.display())],
&[&rule],
&input,
|cmd| {
cmd.assert()
.stdout(format!("a {}\n", input.display()))
.stderr("")
.success();
},
);
}
#[test]
#[cfg(feature = "cuckoo")]
fn test_invalid_module_data() {
let temp = TempDir::new().unwrap();
let rule = temp.path().join("rule");
fs::write(&rule, "rule a { condition: true }").unwrap();
let data = temp.path().join("data");
fs::write(&data, r"{").unwrap();
let input = temp.path().join("input");
fs::write(&input, "").unwrap();
test_scan(&["-x", "name"], &[&rule], &input, |cmd| {
cmd.assert()
.stdout("")
.stderr(predicate::str::contains(
"invalid value 'name' for '--module-data <MODULE=FILE>': \
missing '=' delimiter",
))
.failure();
});
test_scan(
&[&format!("--module-data=piou={}", data.display())],
&[&rule],
&input,
|cmd| {
cmd.assert()
.stdout("")
.stderr("Cannot set data for unsupported module piou\n")
.failure();
},
);
test_scan(&["-x", "cuckoo=non_existing"], &[&rule], &input, |cmd| {
cmd.assert()
.stderr(predicate::str::contains(
"Unable to read cuckoo data from file non_existing",
))
.failure();
});
test_scan(
&[&format!("--module-data=cuckoo={}", data.display())],
&[&rule],
&input,
|cmd| {
cmd.assert()
.stdout("")
.stderr("The data for the cuckoo module is invalid\n")
.failure();
},
);
}
struct BinHelper {
proc: std::process::Child,
}
impl BinHelper {
fn run(arg: &str) -> Self {
let path = std::env::current_exe().unwrap();
let path = path.parent().unwrap();
let path = path.parent().unwrap();
let path = path.join(if cfg!(windows) {
"boreal-test-helpers.exe"
} else {
"boreal-test-helpers"
});
if !path.exists() {
panic!(
"File {} not found. \
You need to compile the `boreal-test-helpers` crate to run this test",
path.display()
);
}
let mut child = std::process::Command::new(path)
.arg(arg)
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
let mut stdout = BufReader::new(child.stdout.take().unwrap());
let mut buffer = String::new();
loop {
buffer.clear();
stdout.read_line(&mut buffer).unwrap();
if buffer.trim() == "ready" {
break;
}
}
Self { proc: child }
}
fn pid(&self) -> u32 {
self.proc.id()
}
}
impl Drop for BinHelper {
fn drop(&mut self) {
drop(self.proc.kill());
drop(self.proc.wait());
}
}