pub(crate) mod bloom_index;
#[cfg(feature = "daachorse-index")]
pub(crate) mod cross_rule_ac;
mod filters;
#[cfg(test)]
mod tests;
use rsigma_parser::{
ConditionExpr, FilterRule, FilterRuleTarget, LogSource, SigmaCollection, SigmaRule,
};
use crate::compiler::{
CompiledRule, compile_detection, compile_rule, evaluate_rule, evaluate_rule_with_bloom,
};
use crate::error::Result;
use crate::event::Event;
use crate::pipeline::{Pipeline, apply_pipelines};
use crate::result::MatchResult;
use crate::rule_index::RuleIndex;
use bloom_index::{BloomCache, FieldBloomIndex};
use filters::{filter_logsource_contains, logsource_matches, rewrite_condition_identifiers};
pub struct Engine {
rules: Vec<CompiledRule>,
pipelines: Vec<Pipeline>,
include_event: bool,
filter_counter: usize,
rule_index: RuleIndex,
bloom_index: FieldBloomIndex,
bloom_prefilter: bool,
bloom_max_bytes: Option<usize>,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_index: cross_rule_ac::CrossRuleAcIndex,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_enabled: bool,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_prunable: Vec<bool>,
}
impl Engine {
pub fn new() -> Self {
Engine {
rules: Vec::new(),
pipelines: Vec::new(),
include_event: false,
filter_counter: 0,
rule_index: RuleIndex::empty(),
bloom_index: FieldBloomIndex::empty(),
bloom_prefilter: false,
bloom_max_bytes: None,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_index: cross_rule_ac::CrossRuleAcIndex::empty(),
#[cfg(feature = "daachorse-index")]
cross_rule_ac_enabled: false,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_prunable: Vec::new(),
}
}
pub fn new_with_pipeline(pipeline: Pipeline) -> Self {
Engine {
rules: Vec::new(),
pipelines: vec![pipeline],
include_event: false,
filter_counter: 0,
rule_index: RuleIndex::empty(),
bloom_index: FieldBloomIndex::empty(),
bloom_prefilter: false,
bloom_max_bytes: None,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_index: cross_rule_ac::CrossRuleAcIndex::empty(),
#[cfg(feature = "daachorse-index")]
cross_rule_ac_enabled: false,
#[cfg(feature = "daachorse-index")]
cross_rule_ac_prunable: Vec::new(),
}
}
pub fn set_bloom_prefilter(&mut self, enabled: bool) {
self.bloom_prefilter = enabled;
}
pub fn bloom_prefilter_enabled(&self) -> bool {
self.bloom_prefilter
}
pub fn set_bloom_max_bytes(&mut self, max_bytes: usize) {
self.bloom_max_bytes = Some(max_bytes);
if !self.rules.is_empty() {
self.rebuild_index();
}
}
pub fn bloom_max_bytes(&self) -> Option<usize> {
self.bloom_max_bytes
}
#[cfg(feature = "daachorse-index")]
pub fn set_cross_rule_ac(&mut self, enabled: bool) {
self.cross_rule_ac_enabled = enabled;
if enabled && !self.rules.is_empty() {
self.rebuild_index();
}
}
#[cfg(feature = "daachorse-index")]
pub fn cross_rule_ac_enabled(&self) -> bool {
self.cross_rule_ac_enabled
}
pub fn set_include_event(&mut self, include: bool) {
self.include_event = include;
}
pub fn add_pipeline(&mut self, pipeline: Pipeline) {
self.pipelines.push(pipeline);
self.pipelines.sort_by_key(|p| p.priority);
}
pub fn add_rule(&mut self, rule: &SigmaRule) -> Result<()> {
let compiled = if self.pipelines.is_empty() {
compile_rule(rule)?
} else {
let mut transformed = rule.clone();
apply_pipelines(&self.pipelines, &mut transformed)?;
compile_rule(&transformed)?
};
self.rules.push(compiled);
self.rebuild_index();
Ok(())
}
pub fn add_collection(&mut self, collection: &SigmaCollection) -> Result<()> {
for rule in &collection.rules {
let compiled = if self.pipelines.is_empty() {
compile_rule(rule)?
} else {
let mut transformed = rule.clone();
apply_pipelines(&self.pipelines, &mut transformed)?;
compile_rule(&transformed)?
};
self.rules.push(compiled);
}
for filter in &collection.filters {
self.apply_filter_no_rebuild(filter)?;
}
self.rebuild_index();
Ok(())
}
pub fn add_collection_with_pipelines(
&mut self,
collection: &SigmaCollection,
pipelines: &[Pipeline],
) -> Result<()> {
let prev = std::mem::take(&mut self.pipelines);
self.pipelines = pipelines.to_vec();
self.pipelines.sort_by_key(|p| p.priority);
let result = self.add_collection(collection);
self.pipelines = prev;
result
}
pub fn apply_filter(&mut self, filter: &FilterRule) -> Result<()> {
self.apply_filter_no_rebuild(filter)?;
self.rebuild_index();
Ok(())
}
fn apply_filter_no_rebuild(&mut self, filter: &FilterRule) -> Result<()> {
let mut filter_detections = Vec::new();
for (name, detection) in &filter.detection.named {
let compiled = compile_detection(detection)?;
filter_detections.push((name.clone(), compiled));
}
if filter_detections.is_empty() {
return Ok(());
}
let fc = self.filter_counter;
self.filter_counter += 1;
let rewritten_cond = if let Some(cond_expr) = filter.detection.conditions.first() {
rewrite_condition_identifiers(cond_expr, fc)
} else {
if filter_detections.len() == 1 {
ConditionExpr::Identifier(format!("__filter_{fc}_{}", filter_detections[0].0))
} else {
ConditionExpr::And(
filter_detections
.iter()
.map(|(name, _)| ConditionExpr::Identifier(format!("__filter_{fc}_{name}")))
.collect(),
)
}
};
let mut matched_any = false;
for rule in &mut self.rules {
let rule_matches = match &filter.rules {
FilterRuleTarget::Any => true,
FilterRuleTarget::Specific(refs) => refs
.iter()
.any(|r| rule.id.as_deref() == Some(r.as_str()) || rule.title == *r),
};
if rule_matches {
if let Some(ref filter_ls) = filter.logsource
&& !filter_logsource_contains(filter_ls, &rule.logsource)
{
continue;
}
for (name, compiled) in &filter_detections {
rule.detections
.insert(format!("__filter_{fc}_{name}"), compiled.clone());
}
rule.conditions = rule
.conditions
.iter()
.map(|cond| ConditionExpr::And(vec![cond.clone(), rewritten_cond.clone()]))
.collect();
matched_any = true;
}
}
if let FilterRuleTarget::Specific(_) = &filter.rules
&& !matched_any
{
log::warn!(
"filter '{}' references rules {:?} but none matched any loaded rule",
filter.title,
filter.rules
);
}
Ok(())
}
pub fn add_compiled_rule(&mut self, rule: CompiledRule) {
self.rules.push(rule);
self.rebuild_index();
}
fn rebuild_index(&mut self) {
self.rule_index = RuleIndex::build(&self.rules);
self.bloom_index = match self.bloom_max_bytes {
Some(budget) => FieldBloomIndex::build_with_budget(&self.rules, budget),
None => FieldBloomIndex::build(&self.rules),
};
#[cfg(feature = "daachorse-index")]
{
if self.cross_rule_ac_enabled {
self.cross_rule_ac_index = cross_rule_ac::CrossRuleAcIndex::build(&self.rules);
self.cross_rule_ac_prunable = self
.rules
.iter()
.map(cross_rule_ac::rule_is_ac_prunable)
.collect();
} else {
self.cross_rule_ac_index = cross_rule_ac::CrossRuleAcIndex::empty();
self.cross_rule_ac_prunable.clear();
}
}
}
pub fn evaluate<E: Event>(&self, event: &E) -> Vec<MatchResult> {
if self.bloom_prefilter {
self.evaluate_with_bloom_path(event)
} else {
self.evaluate_no_bloom_path(event)
}
}
#[cfg(feature = "daachorse-index")]
fn cross_rule_ac_keep_mask<E: Event>(&self, event: &E) -> Option<Vec<bool>> {
if !self.cross_rule_ac_enabled || self.cross_rule_ac_index.is_empty() {
return None;
}
let mut hits = vec![false; self.rules.len()];
self.cross_rule_ac_index.mark_hits(event, &mut hits);
for (idx, slot) in hits.iter_mut().enumerate() {
if !self
.cross_rule_ac_prunable
.get(idx)
.copied()
.unwrap_or(false)
{
*slot = true;
}
}
Some(hits)
}
#[cfg(not(feature = "daachorse-index"))]
#[inline(always)]
fn cross_rule_ac_keep_mask<E: Event>(&self, _event: &E) -> Option<Vec<bool>> {
None
}
fn evaluate_no_bloom_path<E: Event>(&self, event: &E) -> Vec<MatchResult> {
let keep = self.cross_rule_ac_keep_mask(event);
let mut results = Vec::new();
for idx in self.rule_index.candidates(event) {
if let Some(ref mask) = keep
&& !mask[idx]
{
continue;
}
let rule = &self.rules[idx];
if let Some(mut m) = evaluate_rule(rule, event) {
if self.include_event && m.event.is_none() {
m.event = Some(event.to_json());
}
results.push(m);
}
}
results
}
fn evaluate_with_bloom_path<E: Event>(&self, event: &E) -> Vec<MatchResult> {
let bloom = BloomCache::new(&self.bloom_index, event);
let keep = self.cross_rule_ac_keep_mask(event);
let mut results = Vec::new();
for idx in self.rule_index.candidates(event) {
if let Some(ref mask) = keep
&& !mask[idx]
{
continue;
}
let rule = &self.rules[idx];
if let Some(mut m) = evaluate_rule_with_bloom(rule, event, &bloom) {
if self.include_event && m.event.is_none() {
m.event = Some(event.to_json());
}
results.push(m);
}
}
results
}
pub fn evaluate_with_logsource<E: Event>(
&self,
event: &E,
event_logsource: &LogSource,
) -> Vec<MatchResult> {
if self.bloom_prefilter {
self.evaluate_with_logsource_with_bloom(event, event_logsource)
} else {
self.evaluate_with_logsource_no_bloom(event, event_logsource)
}
}
fn evaluate_with_logsource_no_bloom<E: Event>(
&self,
event: &E,
event_logsource: &LogSource,
) -> Vec<MatchResult> {
let keep = self.cross_rule_ac_keep_mask(event);
let mut results = Vec::new();
for idx in self.rule_index.candidates(event) {
if let Some(ref mask) = keep
&& !mask[idx]
{
continue;
}
let rule = &self.rules[idx];
if logsource_matches(&rule.logsource, event_logsource)
&& let Some(mut m) = evaluate_rule(rule, event)
{
if self.include_event && m.event.is_none() {
m.event = Some(event.to_json());
}
results.push(m);
}
}
results
}
fn evaluate_with_logsource_with_bloom<E: Event>(
&self,
event: &E,
event_logsource: &LogSource,
) -> Vec<MatchResult> {
let bloom = BloomCache::new(&self.bloom_index, event);
let keep = self.cross_rule_ac_keep_mask(event);
let mut results = Vec::new();
for idx in self.rule_index.candidates(event) {
if let Some(ref mask) = keep
&& !mask[idx]
{
continue;
}
let rule = &self.rules[idx];
if logsource_matches(&rule.logsource, event_logsource)
&& let Some(mut m) = evaluate_rule_with_bloom(rule, event, &bloom)
{
if self.include_event && m.event.is_none() {
m.event = Some(event.to_json());
}
results.push(m);
}
}
results
}
pub fn evaluate_batch<E: Event + Sync>(&self, events: &[&E]) -> Vec<Vec<MatchResult>> {
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
events.par_iter().map(|e| self.evaluate(e)).collect()
}
#[cfg(not(feature = "parallel"))]
{
events.iter().map(|e| self.evaluate(e)).collect()
}
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn rules(&self) -> &[CompiledRule] {
&self.rules
}
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}