Skip to content

Commit

Permalink
feat: add CompilerBuilder object
Browse files Browse the repository at this point in the history
Add a builder for the compiler object. This builder is only used
for the moment to add modules, but more will be added.

This builder solves an issue when adding modules, in that existing
modules could not be replaced by the one added, which made for a much
worse API and the impossibility of overwriting a module configuration
with a new one.

This is now solved as with a CompilerBuilder, it is guaranteed that
added modules have not yet been used by any rules, so they can be
replaced.

This makes creating a compiler a bit more complex when custom modules
are added, but this is not a huge change.
  • Loading branch information
vthib committed Sep 28, 2024
1 parent 59ef01e commit 261b11c
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 67 deletions.
9 changes: 4 additions & 5 deletions boreal-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use std::process::ExitCode;
use std::thread::JoinHandle;
use std::time::Duration;

use boreal::compiler::ExternalValue;
use boreal::module::Value as ModuleValue;
use boreal::compiler::{CompilerBuilder, ExternalValue};
use boreal::module::{Console, Value as ModuleValue};
use boreal::scanner::{FragmentedScanMode, ScanError, ScanParams, ScanResult};
use boreal::{statistics, Compiler, Metadata, MetadataValue, Scanner};

Expand Down Expand Up @@ -252,16 +252,15 @@ fn main() -> ExitCode {
let mut scanner = {
let rules_file: PathBuf = args.remove_one("rules_file").unwrap();

let mut compiler = Compiler::new();

let no_console_logs = args.get_flag("no_console_logs");
// Even if the console logs are disabled, add the module so that rules that use it
// can still compile properly.
let _r = compiler.add_module(boreal::module::Console::with_callback(move |log| {
let builder = CompilerBuilder::new().add_module(Console::with_callback(move |log| {
if !no_console_logs {
println!("{log}");
}
}));
let mut compiler = builder.build();

compiler.set_params(
boreal::compiler::CompilerParams::default()
Expand Down
94 changes: 94 additions & 0 deletions boreal/src/compiler/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::{collections::HashMap, sync::Arc};

use super::{AvailableModule, ModuleLocation};

/// Configurable builder for the [`Compiler`] object.
#[derive(Debug, Default)]
pub struct CompilerBuilder {
/// Modules that can be imported when compiling rules.
modules: HashMap<&'static str, AvailableModule>,
}

impl CompilerBuilder {
/// Create a new builder with sane default values.
///
/// Modules enabled by default:
/// - `time`
/// - `math`
/// - `string`
/// - `hash` if the `hash` feature is enabled
/// - `elf`, `macho`, `pe`, `dotnet` and `dex` if the `object` feature is enabled
/// - `magic` if the `magic` feature is enabled
/// - `cuckoo` if the `cuckoo` feature is enabled
///
/// Modules disabled by default:
/// - `console`
///
/// To create a builder without any modules, use [`CompilerBuilder::default`] to
/// create a [`CompilerBuilder`] without any modules, then add back only the desired modules.
#[must_use]
pub fn new() -> Self {
let this = Self::default();

let this = this.add_module(crate::module::Time);
let this = this.add_module(crate::module::Math);
let this = this.add_module(crate::module::String_);

#[cfg(feature = "hash")]
let this = this.add_module(crate::module::Hash);

#[cfg(feature = "object")]
let this = this.add_module(crate::module::Pe);
#[cfg(feature = "object")]
let this = this.add_module(crate::module::Elf);
#[cfg(feature = "object")]
let this = this.add_module(crate::module::MachO);
#[cfg(feature = "object")]
let this = this.add_module(crate::module::Dotnet);
#[cfg(feature = "object")]
let this = this.add_module(crate::module::Dex);

#[cfg(feature = "magic")]
let this = this.add_module(crate::module::Magic);

#[cfg(feature = "cuckoo")]
let this = this.add_module(crate::module::Cuckoo);

this
}

/// Add a module that will be importable in rules.
///
/// If the same module has already been added, it will be replaced by this one.
/// This can be useful to change the parameters of a module.
#[must_use]
pub fn add_module<M: crate::module::Module + 'static>(mut self, module: M) -> Self {
let compiled_module = Arc::new(super::module::compile_module(&module));

let _r = self.modules.insert(
compiled_module.name,
AvailableModule {
compiled_module,
location: ModuleLocation::Module(Box::new(module)),
},
);
self
}

/// Build a [`Compiler`] object with the configuration set on this builder.
#[must_use]
pub fn build(self) -> super::Compiler {
super::Compiler::build(self.modules)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::test_type_traits_non_clonable;

#[test]
fn test_types_traits() {
test_type_traits_non_clonable(CompilerBuilder::default());
}
}
46 changes: 7 additions & 39 deletions boreal/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::files::SimpleFile;
use codespan_reporting::term;

mod builder;
pub use builder::CompilerBuilder;
mod error;
pub use error::CompilationError;
pub(crate) mod expression;
Expand Down Expand Up @@ -134,51 +136,17 @@ impl Compiler {
/// create a [`Compiler`] without any modules, then add back only the desired modules.
#[must_use]
pub fn new() -> Self {
let mut this = Self::default();

let _r = this.add_module(crate::module::Time);
let _r = this.add_module(crate::module::Math);
let _r = this.add_module(crate::module::String_);

#[cfg(feature = "hash")]
let _r = this.add_module(crate::module::Hash);

#[cfg(feature = "object")]
let _r = this.add_module(crate::module::Pe);
#[cfg(feature = "object")]
let _r = this.add_module(crate::module::Elf);
#[cfg(feature = "object")]
let _r = this.add_module(crate::module::MachO);
#[cfg(feature = "object")]
let _r = this.add_module(crate::module::Dotnet);
#[cfg(feature = "object")]
let _r = this.add_module(crate::module::Dex);

#[cfg(feature = "magic")]
let _r = this.add_module(crate::module::Magic);

#[cfg(feature = "cuckoo")]
let _r = this.add_module(crate::module::Cuckoo);

this
CompilerBuilder::new().build()
}

/// Add a module.
///
/// Returns false if a module with the same name is already registered, and the module
/// was not added.
pub fn add_module<M: crate::module::Module + 'static>(&mut self, module: M) -> bool {
let m = module::compile_module(&module);

match self.available_modules.entry(m.name) {
Entry::Occupied(_) => false,
Entry::Vacant(v) => {
let _r = v.insert(AvailableModule {
compiled_module: Arc::new(m),
location: ModuleLocation::Module(Box::new(module)),
});
true
}
fn build(available_modules: HashMap<&'static str, AvailableModule>) -> Self {
Self {
available_modules,
..Default::default()
}
}

Expand Down
4 changes: 2 additions & 2 deletions boreal/src/module/cuckoo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use super::{EvalContext, Module, ModuleData, StaticValue, Type, Value};
///
/// ```
/// use boreal::module::{Cuckoo, CuckooData};
/// use boreal::compiler::CompilerBuilder;
///
/// let mut compiler = boreal::Compiler::new();
/// compiler.add_module(Cuckoo);
/// let mut compiler = CompilerBuilder::new().add_module(Cuckoo).build();
/// compiler.add_rules_str(r#"
/// import "cuckoo"
///
Expand Down
9 changes: 4 additions & 5 deletions boreal/src/module/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//!
//! ```
//! use boreal::module::{Module, StaticValue};
//! use boreal::Compiler;
//! use boreal::compiler::CompilerBuilder;
//! use std::collections::HashMap;
//!
//! struct Pi;
Expand All @@ -32,8 +32,7 @@
//! "#;
//!
//! fn main() {
//! let mut compiler = Compiler::new();
//! compiler.add_module(Pi);
//! let mut compiler = CompilerBuilder::new().add_module(Pi).build();
//! assert!(compiler.add_rules_str(RULE).is_ok());
//! }
//! ```
Expand Down Expand Up @@ -339,6 +338,7 @@ impl std::fmt::Debug for ModuleDataMap<'_> {
/// ```
/// use std::collections::HashMap;
/// use boreal::module::{EvalContext, Module, ModuleData, StaticValue, Value, Type};
/// use boreal::compiler::CompilerBuilder;
///
/// struct Bar;
///
Expand Down Expand Up @@ -371,8 +371,7 @@ impl std::fmt::Debug for ModuleDataMap<'_> {
/// }
/// }
///
/// let mut compiler = boreal::Compiler::new();
/// compiler.add_module(Bar);
/// let mut compiler = CompilerBuilder::new().add_module(Bar).build();
/// compiler.add_rules_str(r#"
/// import "bar"
///
Expand Down
5 changes: 2 additions & 3 deletions boreal/src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -881,11 +881,11 @@ impl std::fmt::Display for DefineSymbolError {

#[cfg(test)]
mod tests {
use crate::compiler::CompilerBuilder;
use crate::module::{EvalContext, ScanContext, StaticValue, Type, Value as ModuleValue};
use crate::test_helpers::{
test_type_traits, test_type_traits_non_clonable, test_type_unwind_safe,
};
use crate::Compiler;

use super::*;

Expand Down Expand Up @@ -970,8 +970,7 @@ mod tests {

#[track_caller]
fn test_eval_with_poison(rule_str: &str, mem: &[u8], expected: Option<bool>) {
let mut compiler = Compiler::default();
_ = compiler.add_module(Test);
let mut compiler = CompilerBuilder::default().add_module(Test).build();
let _r = compiler.add_rules_str(rule_str).unwrap();
let scanner = compiler.into_scanner();

Expand Down
38 changes: 34 additions & 4 deletions boreal/tests/it/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Tests for the scanner API.
use std::io::{Seek, Write};
use std::sync::atomic::{AtomicBool, Ordering};

use boreal::compiler::CompilerBuilder;

// An import is reused in the same namespace
#[test]
Expand Down Expand Up @@ -58,8 +61,35 @@ fn test_add_rules_str_err() {
}

#[test]
fn test_compiler_api() {
let mut compiler = boreal::Compiler::default();
assert!(compiler.add_module(boreal::module::Time));
assert!(!compiler.add_module(boreal::module::Time));
fn test_compiler_builder_replace_module() {
static FIRST: AtomicBool = AtomicBool::new(false);
static SECOND: AtomicBool = AtomicBool::new(false);

let builder = CompilerBuilder::default();

// Add the console module twice, to check the second one is used in place of the
// first one.
let builder = builder.add_module(boreal::module::Console::with_callback(|_| {
FIRST.store(true, Ordering::SeqCst)
}));
let builder = builder.add_module(boreal::module::Console::with_callback(|_| {
SECOND.store(true, Ordering::SeqCst)
}));

let mut compiler = builder.build();
compiler
.add_rules_str(
r#"import "console"
rule a {
condition:
console.log("a")
}"#,
)
.unwrap();
let scanner = compiler.into_scanner();
scanner.scan_mem(b"").unwrap();

assert!(!FIRST.load(Ordering::SeqCst));
assert!(SECOND.load(Ordering::SeqCst));
}
11 changes: 7 additions & 4 deletions boreal/tests/it/modules.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::sync::Mutex;

use boreal::compiler::CompilerBuilder;

use crate::utils::{check, check_boreal, check_err, Compiler};

#[track_caller]
Expand Down Expand Up @@ -446,13 +448,14 @@ fn test_module_hash() {
#[test]
fn test_module_console() {
static LOGS: Mutex<Vec<String>> = Mutex::new(Vec::new());

let mut compiler = Compiler::new();
let res = compiler
.compiler
// Replace boreal compiler with a new one to add the console module
compiler.compiler = CompilerBuilder::new()
.add_module(boreal::module::Console::with_callback(|log| {
LOGS.lock().unwrap().push(log);
}));
assert!(res);
}))
.build();

compiler.add_rules(
r#"import "console"
Expand Down
15 changes: 10 additions & 5 deletions boreal/tests/it/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ impl Compiler {
}

fn new_inner(with_yara: bool) -> Self {
let mut compiler = build_compiler();
compiler.add_module(super::module_tests::Tests);
let compiler = build_compiler(true);

let mut this = Self {
compiler,
Expand Down Expand Up @@ -214,8 +213,14 @@ impl Compiler {
}
}

fn build_compiler() -> boreal::Compiler {
boreal::Compiler::new()
fn build_compiler(with_test_module: bool) -> boreal::Compiler {
if with_test_module {
boreal::compiler::CompilerBuilder::new()
.add_module(super::module_tests::Tests)
.build()
} else {
boreal::Compiler::new()
}
}

impl Checker {
Expand Down Expand Up @@ -800,7 +805,7 @@ pub fn compare_module_values_on_mem<M: Module>(
ignored_diffs: &[&str],
) {
// Setup boreal scanner
let mut compiler = build_compiler();
let mut compiler = build_compiler(false);
compiler
.add_rules_str(format!(
"import \"{}\" rule a {{ condition: true }}",
Expand Down

0 comments on commit 261b11c

Please sign in to comment.