Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions boreal-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use pyo3::{create_exception, ffi, intern};
use ::boreal::compiler;

// TODO: all clone impls should be efficient...
// TODO: should all pyclasses have names and be exposed in the module?
// TODO: check GIL handling in all functions (especially match)

mod rule;
Expand All @@ -24,12 +23,29 @@ create_exception!(boreal, AddRuleError, PyException, "error when adding rules");

#[pymodule]
fn boreal(m: &Bound<'_, PyModule>) -> PyResult<()> {
let py = m.py();

m.add_function(wrap_pyfunction!(compile, m)?)?;
m.add_function(wrap_pyfunction!(available_modules, m)?)?;

m.add("AddRuleError", m.py().get_type::<AddRuleError>())?;
m.add("ScanError", m.py().get_type::<scanner::ScanError>())?;
m.add("TimeoutError", m.py().get_type::<scanner::TimeoutError>())?;
m.add("modules", get_available_modules(py))?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;

m.add("AddRuleError", py.get_type::<AddRuleError>())?;
m.add("ScanError", py.get_type::<scanner::ScanError>())?;
m.add("TimeoutError", py.get_type::<scanner::TimeoutError>())?;

m.add("Rule", py.get_type::<rule::Rule>())?;
m.add("Match", py.get_type::<rule_match::Match>())?;
m.add("Scanner", py.get_type::<scanner::Scanner>())?;
m.add("RulesIter", py.get_type::<scanner::RulesIter>())?;
m.add(
"StringMatchInstance",
py.get_type::<string_match_instance::StringMatchInstance>(),
)?;
m.add(
"StringMatches",
py.get_type::<string_matches::StringMatches>(),
)?;

Ok(())
}
Expand Down Expand Up @@ -150,8 +166,7 @@ fn compile(
Ok(scanner::Scanner::new(compiler.into_scanner(), warnings))
}

#[pyfunction]
fn available_modules(py: Python<'_>) -> Vec<Bound<'_, PyString>> {
fn get_available_modules(py: Python<'_>) -> Vec<Bound<'_, PyString>> {
build_compiler()
.available_modules()
.map(|s| PyString::new(py, s))
Expand Down
2 changes: 1 addition & 1 deletion boreal-py/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ::boreal::scanner;
use ::boreal::{Metadata, MetadataValue};

/// A matching rule
#[pyclass(frozen)]
#[pyclass(frozen, module = "boreal")]
pub struct Rule {
/// Name of the rule
#[pyo3(get)]
Expand Down
2 changes: 1 addition & 1 deletion boreal-py/src/rule_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::rule::convert_metadata;
use crate::string_matches::StringMatches;

/// A matching rule
#[pyclass(frozen)]
#[pyclass(frozen, module = "boreal")]
pub struct Match {
/// Name of the matching rule
#[pyo3(get)]
Expand Down
38 changes: 23 additions & 15 deletions boreal-py/src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,18 @@ use crate::rule_match::Match;
create_exception!(boreal, ScanError, PyException, "error when scanning");
create_exception!(boreal, TimeoutError, PyException, "scan timed out");

#[pyclass]
#[pyclass(frozen, module = "boreal")]
pub struct Scanner {
scanner: scanner::Scanner,

/// List of warnings generated when compiling rules.
#[pyo3(get)]
warnings: Vec<String>,

rules_iter: Option<std::vec::IntoIter<crate::rule::Rule>>,
}

impl Scanner {
pub fn new(scanner: scanner::Scanner, warnings: Vec<String>) -> Self {
Self {
scanner,
warnings,
rules_iter: None,
}
Self { scanner, warnings }
}
}

Expand Down Expand Up @@ -140,22 +136,34 @@ impl Scanner {
}
}

fn __iter__(mut slf: PyRefMut<Self>) -> PyResult<PyRefMut<Self>> {
fn __iter__(&self, py: Python<'_>) -> PyResult<RulesIter> {
// Unfortunately, we cannot return an object with a lifetime, so
// we need to collect all rules into a vec of owned elements before
// generating an iterator...
slf.rules_iter = Some(
slf.scanner
Ok(RulesIter {
rules_iter: self
.scanner
.rules()
.map(|rule| crate::rule::Rule::new(slf.py(), &slf.scanner, &rule))
.map(|rule| crate::rule::Rule::new(py, &self.scanner, &rule))
.collect::<Result<Vec<_>, _>>()?
.into_iter(),
);
Ok(slf)
})
}
}

#[pyclass(module = "boreal")]
pub struct RulesIter {
rules_iter: std::vec::IntoIter<crate::rule::Rule>,
}

#[pymethods]
impl RulesIter {
fn __iter__(slf: PyRef<Self>) -> PyRef<Self> {
slf
}

fn __next__(mut slf: PyRefMut<Self>) -> Option<crate::rule::Rule> {
slf.rules_iter.as_mut().and_then(Iterator::next)
slf.rules_iter.next()
}
}

Expand Down
2 changes: 1 addition & 1 deletion boreal-py/src/string_match_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use pyo3::prelude::*;
use ::boreal::scanner;

/// Match instance of a YARA string
#[pyclass(frozen)]
#[pyclass(frozen, module = "boreal")]
#[derive(Clone)]
pub struct StringMatchInstance {
/// Offset of the match.
Expand Down
2 changes: 1 addition & 1 deletion boreal-py/src/string_matches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ::boreal::scanner;
use crate::string_match_instance::StringMatchInstance;

/// List of match instances of a YARA string
#[pyclass(frozen)]
#[pyclass(frozen, module = "boreal")]
#[derive(Clone)]
pub struct StringMatches {
/// Name of the matching string.
Expand Down
17 changes: 17 additions & 0 deletions boreal-py/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
from .utils import MODULES


@pytest.mark.parametrize('module,is_yara', MODULES)
def test_modules(module, is_yara):
modules = module.modules

assert type(modules) is list
assert 'pe' in modules
assert 'time' in modules
assert 'console' in modules


@pytest.mark.parametrize('module,is_yara', MODULES)
def test_version(module, is_yara):
assert type(module.__version__) is str
7 changes: 1 addition & 6 deletions boreal-py/tests/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
import pytest
import tempfile
import yara


MODULES = [
(boreal, False),
(yara, True),
]
from .utils import MODULES


def compile_exc_type(is_yara):
Expand Down
15 changes: 5 additions & 10 deletions boreal-py/tests/test_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
import subprocess
import tempfile
import yara


MODULES = [
(boreal, False),
(yara, True),
]
from .utils import MODULES


def get_rules(module):
Expand Down Expand Up @@ -250,10 +245,10 @@ def test_match_console_log(module, is_yara, capsys):
assert captured.err == ""


# TODO: find a way to compile yara python with cuckoo?
def test_match_modules_data():
@pytest.mark.parametrize('module,is_yara', MODULES)
def test_match_modules_data(module, is_yara):
# Test only works if the cuckoo module is present
if 'cuckoo' not in boreal.available_modules():
if 'cuckoo' not in module.modules:
return

rules = boreal.compile(source="""
Expand All @@ -277,7 +272,7 @@ def test_match_modules_data_errors():
with pytest.raises(TypeError):
rules.match(data="", modules_data={ 'unknown': 1 })

if 'cuckoo' in boreal.available_modules():
if 'cuckoo' in boreal.modules:
with pytest.raises(TypeError):
rules.match(data="", modules_data={ 'cuckoo': 1 })
with pytest.raises(TypeError):
Expand Down
9 changes: 1 addition & 8 deletions boreal-py/tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import boreal
import pytest
import yara


MODULES = [
(boreal, False),
(yara, True),
]
from .utils import MODULES


@pytest.mark.parametrize("module,is_yara", MODULES)
Expand Down
8 changes: 8 additions & 0 deletions boreal-py/tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import boreal
import yara


MODULES = [
(boreal, False),
(yara, True),
]