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};
use crate::error::Result;
use crate::event::Event;
use crate::pipeline::{Pipeline, apply_pipelines};
use crate::result::MatchResult;
use crate::rule_index::RuleIndex;
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,
}
impl Engine {
pub fn new() -> Self {
Engine {
rules: Vec::new(),
pipelines: Vec::new(),
include_event: false,
filter_counter: 0,
rule_index: RuleIndex::empty(),
}
}
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(),
}
}
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);
}
pub fn evaluate<E: Event>(&self, event: &E) -> Vec<MatchResult> {
let mut results = Vec::new();
for idx in self.rule_index.candidates(event) {
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
}
pub fn evaluate_with_logsource<E: Event>(
&self,
event: &E,
event_logsource: &LogSource,
) -> Vec<MatchResult> {
let mut results = Vec::new();
for idx in self.rule_index.candidates(event) {
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
}
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()
}
}