diff --git a/gene/Cargo.toml b/gene/Cargo.toml index 5f98974..b7140fc 100644 --- a/gene/Cargo.toml +++ b/gene/Cargo.toml @@ -9,7 +9,12 @@ repository.workspace = true license.workspace = true readme.workspace = true documentation = "https://docs.rs/gene" -keywords = ["log-processing", "signature-engine", "security", "threat-detection"] +keywords = [ + "log-processing", + "signature-engine", + "security", + "threat-detection", +] [dependencies] gene_derive = { path = "../derive", version = ">=0.1.0" } @@ -24,3 +29,8 @@ serde = { version = "1.0.192", features = ["derive"] } [dev-dependencies] serde_json = "1.0.108" libflate = "2.0.0" +criterion = "0.5.1" + +[[bench]] +name = "engine_benchmark" +harness = false diff --git a/gene/tests/data/.gitignore b/gene/benches/data/.gitignore similarity index 100% rename from gene/tests/data/.gitignore rename to gene/benches/data/.gitignore diff --git a/gene/tests/data/compiled.gen b/gene/benches/data/compiled.gen similarity index 100% rename from gene/tests/data/compiled.gen rename to gene/benches/data/compiled.gen diff --git a/gene/tests/data/events.json.gz b/gene/benches/data/events.json.gz similarity index 100% rename from gene/tests/data/events.json.gz rename to gene/benches/data/events.json.gz diff --git a/gene/tests/rust_events_test.rs b/gene/benches/engine_benchmark.rs similarity index 53% rename from gene/tests/rust_events_test.rs rename to gene/benches/engine_benchmark.rs index 9e18ea5..47eb9b6 100644 --- a/gene/tests/rust_events_test.rs +++ b/gene/benches/engine_benchmark.rs @@ -1,5 +1,10 @@ -use std::{borrow::Cow, collections::HashMap, fs::File, io::Read, time::Duration}; +use std::{ + borrow::Cow, + collections::HashMap, + io::{self, Read}, +}; +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use gene::{Engine, Event, FieldGetter, FieldValue, Rule}; use gene_derive::{Event, FieldGetter}; use libflate::gzip; @@ -39,24 +44,16 @@ struct WinEvent { event: Inner, } -fn time_it(mut f: F) -> Duration { - let start_time = std::time::Instant::now(); - f(); - let end_time = std::time::Instant::now(); - end_time - start_time -} +const RULES: &[u8] = include_bytes!("./data/compiled.gen"); +const EVENTS: &[u8] = include_bytes!("./data/events.json.gz"); + +fn bench_rust_events(c: &mut Criterion) { + let rules = Rule::deserialize_reader(io::Cursor::new(RULES)); -#[cfg_attr( - debug_assertions, - ignore = "long test that must be ran in release mode" -)] -#[test] -fn test_fast() { - let rules = Rule::deserialize_reader(File::open("./tests/data/compiled.gen").unwrap()); let it = rules.into_iter().map(|r| r.unwrap()).collect::>(); // decoding gzip file - let mut dec = gzip::Decoder::new(File::open("./tests/data/events.json.gz").unwrap()).unwrap(); + let mut dec = gzip::Decoder::new(io::Cursor::new(EVENTS)).unwrap(); let mut all = vec![]; dec.read_to_end(&mut all).unwrap(); @@ -65,39 +62,28 @@ fn test_fast() { .map(|e| e.unwrap()) .collect::>(); - let run_count = 5; - let mut engine = Engine::new(); - for i in 0..8 { + + let mut group = c.benchmark_group("scan-throughput"); + group.sample_size(20); + group.throughput(Throughput::Bytes(all.len() as u64)); + for i in 0..1 { for r in it.iter() { let mut r = r.clone(); r.name = format!("{}.{}", r.name, i); engine.insert_rule(r).unwrap(); } - let mut positives = 0; - let scan_dur = time_it(|| { - for _ in 0..run_count { + group.bench_function(&format!("scan-with-{}-rules", engine.rules_count()), |b| { + b.iter(|| { for e in events.iter() { - if let Some(sr) = engine.scan(e).unwrap_or_default() { - if sr.is_detection() { - positives += 1 - } - } + let _ = engine.scan(e); } - } + }) }); - - let events_count = events.len() * run_count; - let mb = (all.len() * run_count) as f64 / 1_000_000_f64; - let mbs = mb / (scan_dur.as_secs_f64()); - let eps = (events_count as f64) / scan_dur.as_secs_f64(); - - println!("Number of scanned events: {events_count} -> {mb:.2} MB"); - println!("Number of loaded rules: {}", engine.rules_count()); - println!("Scan duration: {scan_dur:?} -> {mbs:.2} MB/s -> {eps:.2} events/s",); - println!("Number of detections: {positives}"); - assert!(positives > 0); - println!(); } + group.finish(); } + +criterion_group!(benches, bench_rust_events); +criterion_main!(benches); diff --git a/gene/tests/generic_json_test.rs b/gene/tests/generic_json_test.rs deleted file mode 100644 index 97eb25e..0000000 --- a/gene/tests/generic_json_test.rs +++ /dev/null @@ -1,153 +0,0 @@ -use gene::{Engine, Event, FieldGetter, FieldValue, Rule, XPath}; -use lazy_static::lazy_static; -use libflate::gzip; -use std::{borrow::Cow, fs::File, io::Read, slice, str::FromStr, time::Duration}; - -use serde_json::Value; - -struct JsonEvent { - root: Value, -} - -impl From for JsonEvent { - fn from(value: Value) -> Self { - Self::new(value) - } -} - -lazy_static! { - static ref ID_PATH: XPath = XPath::from_str(".Event.System.EventID").unwrap(); - static ref SRC_PATH: XPath = XPath::from_str(".Event.System.Channel").unwrap(); -} - -impl JsonEvent { - fn new(value: Value) -> Self { - Self { root: value } - } - - fn get_rec(v: &Value, mut next: slice::Iter<'_, std::string::String>) -> Option { - match v { - Value::Number(n) => Some(Value::Number(n.clone())), - Value::Object(m) => Self::get_rec(m.get(next.next()?)?, next), - Value::Array(a) => Some(Value::Array(a.clone())), - Value::Bool(b) => Some(Value::Bool(*b)), - Value::String(s) => Some(Value::String(s.clone())), - Value::Null => Some(Value::Null), - } - } - - fn field_value(value: Value) -> FieldValue { - match value { - Value::String(s) => s.into(), - Value::Bool(b) => b.into(), - Value::Number(n) => match n { - n if n.is_u64() => n.as_u64().unwrap().into(), - n if n.is_f64() => n.as_f64().unwrap().into(), - n if n.is_i64() => n.as_i64().unwrap().into(), - _ => unreachable!(), - }, - Value::Object(_) | Value::Array(_) | Value::Null => unreachable!(), - } - } -} - -impl FieldGetter for JsonEvent { - #[inline] - fn get_from_iter(&self, i: core::slice::Iter<'_, std::string::String>) -> Option { - Some(Self::field_value(Self::get_rec(&self.root, i)?)) - } -} - -impl Event for JsonEvent { - #[inline] - fn id(&self) -> i64 { - if let FieldValue::Number(num) = self.get_from_path(&ID_PATH).unwrap() { - return num.try_into().unwrap(); - } - match self.get_from_path(&ID_PATH).unwrap() { - FieldValue::Number(num) => num.try_into().unwrap(), - FieldValue::String(s) => s.parse::().unwrap(), - _ => panic!("cannot get event id"), - } - } - - #[inline] - fn source(&self) -> Cow<'_, str> { - if let FieldValue::String(src) = self.get_from_path(&SRC_PATH).unwrap() { - return src.into(); - } - panic!("cannot get event source") - } -} - -fn time_it(mut f: F) -> Duration { - let start_time = std::time::Instant::now(); - f(); - let end_time = std::time::Instant::now(); - end_time - start_time -} - -#[cfg_attr( - debug_assertions, - ignore = "long test that must be ran in release mode" -)] -#[test] -fn test_engine() { - let mut engine = Engine::new(); - let rules = Rule::deserialize_reader(File::open("./tests/data/compiled.gen").unwrap()); - let it = rules.into_iter().map(|r| r.unwrap()).collect::>(); - - for i in 0..4 { - for r in it.iter() { - let mut r = r.clone(); - r.name = format!("{}.{}", r.name, i); - engine.insert_rule(r).unwrap(); - } - } - - // decoding gzip file - let mut dec = gzip::Decoder::new(File::open("./tests/data/events.json.gz").unwrap()).unwrap(); - let mut all = vec![]; - dec.read_to_end(&mut all).unwrap(); - - let des = serde_json::Deserializer::from_slice(all.as_slice()).into_iter::(); - - // loading all events - let events: Vec = des.map(|v| JsonEvent::from(v.unwrap())).collect(); - - let run_count = 5; - - let mut engine = Engine::new(); - for i in 0..8 { - for r in it.iter() { - let mut r = r.clone(); - r.name = format!("{}.{}", r.name, i); - engine.insert_rule(r).unwrap(); - } - - let mut positives = 0; - let scan_dur = time_it(|| { - for _ in 0..run_count { - for e in events.iter() { - if let Some(sr) = engine.scan(e).ok().and_then(|v| v) { - if sr.is_detection() { - positives += 1 - } - } - } - } - }); - - let events_count = events.len() * run_count; - let mb = (all.len() * run_count) as f64 / 1_000_000_f64; - let mbs = mb / (scan_dur.as_secs_f64()); - let eps = (events_count as f64) / scan_dur.as_secs_f64(); - - println!("Number of scanned events: {events_count} -> {mb:.2} MB"); - println!("Number of loaded rules: {}", engine.rules_count()); - println!("Scan duration: {scan_dur:?} -> {mbs:.2} MB/s -> {eps:.2} events/s",); - println!("Number of detections: {positives}"); - assert!(positives > 0); - println!(); - } -}