Skip to content

Commit

Permalink
feat: implement console module.
Browse files Browse the repository at this point in the history
  • Loading branch information
plusvic committed Jan 16, 2024
1 parent 23dd10f commit fbde32a
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- build: no-default-features
os: ubuntu-latest
rust: stable
args: "--package yara-x --no-default-features --features=test_proto2-module,test_proto3-module,time-module,hash-module,macho-module,math-module,lnk-module,elf-module,pe-module,dotnet-module"
args: "--package yara-x --no-default-features --features=test_proto2-module,test_proto3-module,time-module,hash-module,macho-module,math-module,lnk-module,elf-module,pe-module,dotnet-module,console-module"

steps:
- name: Checkout sources
Expand Down
19 changes: 19 additions & 0 deletions yara-x-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ impl Scanner {
self.inner.timeout(Duration::from_secs(seconds));
}

/// Sets a callback that is invoked every time a YARA rule calls the
/// `console` module.
///
/// The `callback` function is invoked with a string representing the
/// message being logged. The function can print the message to stdout,
/// append it to a file, etc. If no callback is set these messages are
/// ignored.
fn console_log(&mut self, callback: PyObject) -> PyResult<()> {
if !Python::with_gil(|py| callback.as_ref(py).is_callable()) {
return Err(PyValueError::new_err("callback is not callable"));
}
self.inner.console_log(move |msg| {
let _ = Python::with_gil(|py| -> PyResult<PyObject> {
callback.call1(py, (msg,))
});
});
Ok(())
}

/// Scans in-memory data.
#[pyo3(signature = (data))]
fn scan(&mut self, data: &[u8]) -> PyResult<Py<PyTuple>> {
Expand Down
14 changes: 14 additions & 0 deletions yara-x-py/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,17 @@ def test_scanner_timeout():
scanner.timeout(1)
with pytest.raises(Exception, match='timeout'):
scanner.scan(b'foobar')


def test_console_log():
ok = False
def callback(msg):
nonlocal ok
if msg == 'foo':
ok = True
compiler = yara_x.Compiler()
compiler.add_source('import "console" rule foo {condition: console.log("foo")}')
scanner = yara_x.Scanner(compiler.build())
scanner.console_log(callback)
scanner.scan(b'')
assert ok
7 changes: 5 additions & 2 deletions yara-x/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ logging = ["dep:log"]

# Enables rules profiling. When this is enabled together with `logging` the
# logs will contain information about the most expensive rules after each
# scan. Notice that profiling itself has an noticeable impact on performance.
# scan. Notice that profiling itself has a noticeable impact on performance.
rules-profiling = ["logging"]

# Features for enabling/disabling modules.
Expand All @@ -45,10 +45,12 @@ rules-profiling = ["logging"]
# a given module is built or not. For instance, if the feature `foo-module` is
# enabled, the module `foo` will be built into YARA.

# The `console` module exports functions for printing text from YARA rules.
console-module = []

# The `dotnet` module parsers .NET files.
dotnet-module = []


# The `elf` module parses ELF files.
elf-module = [
"dep:tlsh-fixed"
Expand Down Expand Up @@ -107,6 +109,7 @@ default = [
"constant-folding",
"exact-atoms",
"fast-regexp",
"console-module",
"dotnet-module",
"elf-module",
"macho-module",
Expand Down
112 changes: 112 additions & 0 deletions yara-x/src/modules/console.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use crate::modules::prelude::*;
use crate::modules::protos::console::*;

#[module_main]
fn main(_data: &[u8]) -> Console {
// Nothing to do, but we have to return our protobuf
Console::new()
}

#[module_export(name = "log")]
fn log_str(ctx: &mut ScanContext, string: RuntimeString) -> bool {
ctx.console_log(format!("{}", string.as_bstr(ctx)));
true
}

#[module_export(name = "log")]
fn log_msg_str(
ctx: &mut ScanContext,
message: RuntimeString,
string: RuntimeString,
) -> bool {
ctx.console_log(format!(
"{}{}",
message.as_bstr(ctx),
string.as_bstr(ctx)
));
true
}

#[module_export(name = "log")]
fn log_int(ctx: &mut ScanContext, i: i64) -> bool {
ctx.console_log(format!("{}", i));
true
}

#[module_export(name = "log")]
fn log_msg_int(ctx: &mut ScanContext, message: RuntimeString, i: i64) -> bool {
ctx.console_log(format!("{}{}", message.as_bstr(ctx), i));
true
}

#[module_export(name = "log")]
fn log_float(ctx: &mut ScanContext, f: f64) -> bool {
ctx.console_log(format!("{}", f));
true
}

#[module_export(name = "log")]
fn log_msg_float(
ctx: &mut ScanContext,
message: RuntimeString,
f: f64,
) -> bool {
ctx.console_log(format!("{}{}", message.as_bstr(ctx), f));
true
}

#[module_export(name = "hex")]
fn log_hex(ctx: &mut ScanContext, i: i64) -> bool {
ctx.console_log(format!("0x{:x}", i));
true
}

#[module_export(name = "hex")]
fn log_msg_hex(ctx: &mut ScanContext, message: RuntimeString, i: i64) -> bool {
ctx.console_log(format!("{}0x{:x}", message.as_bstr(ctx), i));
true
}

#[cfg(test)]
mod tests {

#[test]
fn log() {
let rules = crate::compile(
r#"
import "console"
rule test {
condition:
console.log("foo") and
console.log("bar: ", 1) and
console.log("baz: ", 3.14) and
console.log(10) and
console.log(6.28) and
console.hex(10) and
console.hex("qux: ", 255)
}
"#,
)
.unwrap();

let mut messages = vec![];

crate::scanner::Scanner::new(&rules)
.console_log(|message| messages.push(message))
.scan(b"")
.expect("scan should not fail");

assert_eq!(
messages,
vec![
"foo",
"bar: 1",
"baz: 3.14",
"10",
"6.28",
"0xa",
"qux: 0xff"
]
);
}
}
30 changes: 16 additions & 14 deletions yara-x/src/modules/modules.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
// File generated automatically by build.rs. Do not edit.
#[cfg(feature = "test_proto2-module")]
mod test_proto2;
#[cfg(feature = "string-module")]
mod string;
#[cfg(feature = "macho-module")]
mod macho;
#[cfg(feature = "pe-module")]
mod pe;
#[cfg(feature = "elf-module")]
mod elf;
#[cfg(feature = "text-module")]
mod text;
#[cfg(feature = "dotnet-module")]
mod dotnet;
#[cfg(feature = "lnk-module")]
mod lnk;
#[cfg(feature = "hash-module")]
mod hash;
#[cfg(feature = "text-module")]
mod text;
#[cfg(feature = "math-module")]
mod math;
#[cfg(feature = "test_proto2-module")]
mod test_proto2;
#[cfg(feature = "time-module")]
mod time;
#[cfg(feature = "dotnet-module")]
mod dotnet;
#[cfg(feature = "test_proto3-module")]
mod test_proto3;
#[cfg(feature = "pe-module")]
mod pe;
#[cfg(feature = "string-module")]
mod string;
#[cfg(feature = "elf-module")]
mod elf;
#[cfg(feature = "math-module")]
mod math;
#[cfg(feature = "console-module")]
mod console;
14 changes: 14 additions & 0 deletions yara-x/src/modules/protos/console.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto2";
import "yara.proto";

package console;

option (yara.module_options) = {
name : "console"
root_message: "console.Console"
rust_module: "console"
};

message Console {
// This module contains only exported functions, and doesn't return any data
}
8 changes: 8 additions & 0 deletions yara-x/src/scanner/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub(crate) struct ScanContext<'r> {
/// is evaluated, it is compiled the first time and stored in this hash
/// map.
pub regexp_cache: RefCell<FxHashMap<RegexpId, Regex>>,
/// Callback invoked every time a YARA rule calls `console.log`.
pub console_log: Option<Box<dyn FnMut(String) + 'r>>,
/// Hash map that tracks the time spend on each pattern. Keys are pattern
/// PatternIds and values are the cumulative time spent on verifying each
/// pattern.
Expand Down Expand Up @@ -206,6 +208,12 @@ impl ScanContext<'_> {
info!("Started rule evaluation: {}:{}", rule_namespace, rule_name);
}

pub(crate) fn console_log(&mut self, message: String) {
if let Some(console_log) = &mut self.console_log {
console_log(message)
}
}

pub(crate) fn store_struct(
&mut self,
s: Rc<Struct>,
Expand Down
16 changes: 16 additions & 0 deletions yara-x/src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ impl<'r> Scanner<'r> {
wasm_store: NonNull::dangling(),
runtime_objects: IndexMap::new(),
compiled_rules: rules,
console_log: None,
current_struct: None,
root_struct: rules.globals().make_root(),
scanned_data: null(),
Expand Down Expand Up @@ -276,6 +277,21 @@ impl<'r> Scanner<'r> {
self
}

/// Sets a callback that is invoked every time a YARA rule calls the
/// `console` module.
///
/// The `callback` function is invoked with a string representing the
/// message being logged. The function can print the message to stdout,
/// append it to a file, etc. If no callback is set these messages are
/// ignored.
pub fn console_log<F>(&mut self, callback: F) -> &mut Self
where
F: FnMut(String) + 'r,
{
self.wasm_store.data_mut().console_log = Some(Box::new(callback));
self
}

/// Scans a file.
pub fn scan_file<'a, P>(
&'a mut self,
Expand Down
24 changes: 12 additions & 12 deletions yara-x/src/wasm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,38 +470,38 @@ mod tests {
assert_eq!(
text,
r#"(module
(func (;152;) (type 1) (result i32)
(func (;160;) (type 1) (result i32)
i32.const 0
global.set 2
i32.const 0
global.set 3
call 153
call 154
call 161
call 162
global.get 3
)
(func (;153;) (type 0)
(func (;161;) (type 0)
block ;; label = @1
call 155
call 163
end
block ;; label = @1
call 156
call 164
end
)
(func (;154;) (type 0)
(func (;162;) (type 0)
block ;; label = @1
call 157
call 165
end
)
(func (;155;) (type 0)
(func (;163;) (type 0)
i32.const 4
)
(func (;156;) (type 0)
(func (;164;) (type 0)
i32.const 5
)
(func (;157;) (type 0)
(func (;165;) (type 0)
i32.const 6
)
(export "main" (func 152))
(export "main" (func 160))
)"#
);
}
Expand Down

0 comments on commit fbde32a

Please sign in to comment.