use rsigma_parser::{ConditionExpr, FilterRule, 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;
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 filter_cond = 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 = filter.rules.is_empty() || filter.rules.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
&& !logsource_compatible(&rule.logsource, filter_ls)
{
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(),
ConditionExpr::Not(Box::new(filter_cond.clone())),
])
})
.collect();
matched_any = true;
}
}
if !filter.rules.is_empty() && !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(&self, event: &Event) -> 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.as_value().clone());
}
results.push(m);
}
}
results
}
pub fn evaluate_with_logsource(
&self,
event: &Event,
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.as_value().clone());
}
results.push(m);
}
}
results
}
pub fn evaluate_batch<'a>(&self, events: &[&'a Event<'a>]) -> 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()
}
}
fn logsource_compatible(a: &LogSource, b: &LogSource) -> bool {
fn field_compatible(a: &Option<String>, b: &Option<String>) -> bool {
match (a, b) {
(Some(va), Some(vb)) => va.eq_ignore_ascii_case(vb),
_ => true, }
}
field_compatible(&a.category, &b.category)
&& field_compatible(&a.product, &b.product)
&& field_compatible(&a.service, &b.service)
}
fn logsource_matches(rule_ls: &LogSource, event_ls: &LogSource) -> bool {
if let Some(ref cat) = rule_ls.category {
match &event_ls.category {
Some(ec) if ec.eq_ignore_ascii_case(cat) => {}
_ => return false,
}
}
if let Some(ref prod) = rule_ls.product {
match &event_ls.product {
Some(ep) if ep.eq_ignore_ascii_case(prod) => {}
_ => return false,
}
}
if let Some(ref svc) = rule_ls.service {
match &event_ls.service {
Some(es) if es.eq_ignore_ascii_case(svc) => {}
_ => return false,
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use rsigma_parser::parse_sigma_yaml;
use serde_json::json;
fn make_engine_with_rule(yaml: &str) -> Engine {
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
engine
}
#[test]
fn test_simple_match() {
let engine = make_engine_with_rule(
r#"
title: Detect Whoami
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: medium
"#,
);
let ev = json!({"CommandLine": "cmd /c whoami /all"});
let event = Event::from_value(&ev);
let matches = engine.evaluate(&event);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].rule_title, "Detect Whoami");
}
#[test]
fn test_no_match() {
let engine = make_engine_with_rule(
r#"
title: Detect Whoami
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: medium
"#,
);
let ev = json!({"CommandLine": "ipconfig /all"});
let event = Event::from_value(&ev);
let matches = engine.evaluate(&event);
assert!(matches.is_empty());
}
#[test]
fn test_and_not_filter() {
let engine = make_engine_with_rule(
r#"
title: Suspicious Process
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'whoami'
filter:
User: 'SYSTEM'
condition: selection and not filter
level: high
"#,
);
let ev = json!({"CommandLine": "whoami", "User": "admin"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
let ev2 = json!({"CommandLine": "whoami", "User": "SYSTEM"});
let event2 = Event::from_value(&ev2);
assert!(engine.evaluate(&event2).is_empty());
}
#[test]
fn test_multiple_values_or() {
let engine = make_engine_with_rule(
r#"
title: Recon Commands
logsource:
product: windows
detection:
selection:
CommandLine|contains:
- 'whoami'
- 'ipconfig'
- 'net user'
condition: selection
level: medium
"#,
);
let ev = json!({"CommandLine": "ipconfig /all"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
let ev2 = json!({"CommandLine": "dir"});
let event2 = Event::from_value(&ev2);
assert!(engine.evaluate(&event2).is_empty());
}
#[test]
fn test_logsource_routing() {
let engine = make_engine_with_rule(
r#"
title: Windows Process
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: medium
"#,
);
let ev = json!({"CommandLine": "whoami"});
let event = Event::from_value(&ev);
let ls_match = LogSource {
product: Some("windows".into()),
category: Some("process_creation".into()),
..Default::default()
};
assert_eq!(engine.evaluate_with_logsource(&event, &ls_match).len(), 1);
let ls_nomatch = LogSource {
product: Some("linux".into()),
category: Some("process_creation".into()),
..Default::default()
};
assert!(
engine
.evaluate_with_logsource(&event, &ls_nomatch)
.is_empty()
);
}
#[test]
fn test_selector_1_of() {
let engine = make_engine_with_rule(
r#"
title: Multiple Selections
logsource:
product: windows
detection:
selection_cmd:
CommandLine|contains: 'cmd'
selection_ps:
CommandLine|contains: 'powershell'
condition: 1 of selection_*
level: medium
"#,
);
let ev = json!({"CommandLine": "powershell.exe -enc"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
}
#[test]
fn test_filter_rule_application() {
let yaml = r#"
title: Suspicious Process
id: rule-001
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: high
---
title: Filter SYSTEM
filter:
rules:
- rule-001
selection:
User: 'SYSTEM'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
assert_eq!(collection.rules.len(), 1);
assert_eq!(collection.filters.len(), 1);
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let ev = json!({"CommandLine": "whoami", "User": "admin"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
let ev2 = json!({"CommandLine": "whoami", "User": "SYSTEM"});
let event2 = Event::from_value(&ev2);
assert!(engine.evaluate(&event2).is_empty());
}
#[test]
fn test_filter_rule_no_ref_applies_to_all() {
let yaml = r#"
title: Detection A
id: det-a
logsource:
product: windows
detection:
sel:
EventType: alert
condition: sel
---
title: Filter Out Test Env
filter:
rules: []
selection:
Environment: 'test'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let ev = json!({"EventType": "alert", "Environment": "prod"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
let ev2 = json!({"EventType": "alert", "Environment": "test"});
let event2 = Event::from_value(&ev2);
assert!(engine.evaluate(&event2).is_empty());
}
#[test]
fn test_multiple_rules() {
let yaml = r#"
title: Rule A
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: low
---
title: Rule B
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'ipconfig'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
assert_eq!(engine.rule_count(), 2);
let ev = json!({"CommandLine": "whoami"});
let event = Event::from_value(&ev);
let matches = engine.evaluate(&event);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].rule_title, "Rule A");
}
#[test]
fn test_filter_by_rule_name() {
let yaml = r#"
title: Detect Mimikatz
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'mimikatz'
condition: selection
level: critical
---
title: Exclude Admin Tools
filter:
rules:
- Detect Mimikatz
selection:
ParentImage|endswith: '\admin_toolkit.exe'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let ev = json!({"CommandLine": "mimikatz.exe", "ParentImage": "C:\\cmd.exe"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
let ev2 = json!({"CommandLine": "mimikatz.exe", "ParentImage": "C:\\admin_toolkit.exe"});
let event2 = Event::from_value(&ev2);
assert!(engine.evaluate(&event2).is_empty());
}
#[test]
fn test_filter_multiple_detections() {
let yaml = r#"
title: Suspicious Network
id: net-001
logsource:
product: windows
detection:
selection:
DestinationPort: 443
condition: selection
level: medium
---
title: Exclude Trusted
filter:
rules:
- net-001
trusted_dst:
DestinationIp|startswith: '10.'
trusted_user:
User: 'svc_account'
condition: trusted_dst and trusted_user
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let ev = json!({"DestinationPort": 443, "DestinationIp": "8.8.8.8", "User": "admin"});
let event = Event::from_value(&ev);
assert_eq!(engine.evaluate(&event).len(), 1);
let ev2 = json!({"DestinationPort": 443, "DestinationIp": "10.0.0.1", "User": "admin"});
let event2 = Event::from_value(&ev2);
assert_eq!(engine.evaluate(&event2).len(), 1);
let ev3 =
json!({"DestinationPort": 443, "DestinationIp": "10.0.0.1", "User": "svc_account"});
let event3 = Event::from_value(&ev3);
assert!(engine.evaluate(&event3).is_empty());
}
#[test]
fn test_filter_applied_to_multiple_rules() {
let yaml = r#"
title: Rule One
id: r1
logsource:
product: windows
detection:
sel:
EventID: 1
condition: sel
---
title: Rule Two
id: r2
logsource:
product: windows
detection:
sel:
EventID: 2
condition: sel
---
title: Exclude Test
filter:
rules: []
selection:
Environment: 'test'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let ev1 = json!({"EventID": 1, "Environment": "prod"});
assert_eq!(engine.evaluate(&Event::from_value(&ev1)).len(), 1);
let ev2 = json!({"EventID": 2, "Environment": "prod"});
assert_eq!(engine.evaluate(&Event::from_value(&ev2)).len(), 1);
let ev3 = json!({"EventID": 1, "Environment": "test"});
assert!(engine.evaluate(&Event::from_value(&ev3)).is_empty());
let ev4 = json!({"EventID": 2, "Environment": "test"});
assert!(engine.evaluate(&Event::from_value(&ev4)).is_empty());
}
#[test]
fn test_expand_modifier_yaml() {
let yaml = r#"
title: User Profile Access
logsource:
product: windows
detection:
selection:
TargetFilename|expand: 'C:\Users\%username%\AppData\sensitive.dat'
condition: selection
level: high
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({
"TargetFilename": "C:\\Users\\admin\\AppData\\sensitive.dat",
"username": "admin"
});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({
"TargetFilename": "C:\\Users\\admin\\AppData\\sensitive.dat",
"username": "guest"
});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_expand_modifier_multiple_placeholders() {
let yaml = r#"
title: Registry Path
logsource:
product: windows
detection:
selection:
RegistryKey|expand: 'HKLM\SOFTWARE\%vendor%\%product%'
condition: selection
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({
"RegistryKey": "HKLM\\SOFTWARE\\Acme\\Widget",
"vendor": "Acme",
"product": "Widget"
});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({
"RegistryKey": "HKLM\\SOFTWARE\\Acme\\Widget",
"vendor": "Other",
"product": "Widget"
});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_timestamp_hour_modifier_yaml() {
let yaml = r#"
title: Off-Hours Login
logsource:
product: windows
detection:
selection:
EventType: 'login'
time_filter:
Timestamp|hour: 3
condition: selection and time_filter
level: high
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"EventType": "login", "Timestamp": "2024-07-10T03:45:00Z"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"EventType": "login", "Timestamp": "2024-07-10T14:45:00Z"});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_timestamp_day_modifier_yaml() {
let yaml = r#"
title: Weekend Activity
logsource:
product: windows
detection:
selection:
EventType: 'access'
day_check:
CreatedAt|day: 25
condition: selection and day_check
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"EventType": "access", "CreatedAt": "2024-12-25T10:00:00Z"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"EventType": "access", "CreatedAt": "2024-12-26T10:00:00Z"});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_timestamp_year_modifier_yaml() {
let yaml = r#"
title: Legacy System
logsource:
product: windows
detection:
selection:
EventType: 'auth'
old_events:
EventTime|year: 2020
condition: selection and old_events
level: low
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"EventType": "auth", "EventTime": "2020-06-15T10:00:00Z"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"EventType": "auth", "EventTime": "2024-06-15T10:00:00Z"});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_action_repeat_evaluates_correctly() {
let yaml = r#"
title: Detect Whoami
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: medium
---
action: repeat
title: Detect Ipconfig
detection:
selection:
CommandLine|contains: 'ipconfig'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
assert_eq!(collection.rules.len(), 2);
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
assert_eq!(engine.rule_count(), 2);
let ev1 = json!({"CommandLine": "whoami /all"});
let matches1 = engine.evaluate(&Event::from_value(&ev1));
assert_eq!(matches1.len(), 1);
assert_eq!(matches1[0].rule_title, "Detect Whoami");
let ev2 = json!({"CommandLine": "ipconfig /all"});
let matches2 = engine.evaluate(&Event::from_value(&ev2));
assert_eq!(matches2.len(), 1);
assert_eq!(matches2[0].rule_title, "Detect Ipconfig");
let ev3 = json!({"CommandLine": "dir"});
assert!(engine.evaluate(&Event::from_value(&ev3)).is_empty());
}
#[test]
fn test_action_repeat_with_global() {
let yaml = r#"
action: global
logsource:
product: windows
category: process_creation
level: high
---
title: Detect Net User
detection:
selection:
CommandLine|contains: 'net user'
condition: selection
---
action: repeat
title: Detect Net Group
detection:
selection:
CommandLine|contains: 'net group'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
assert_eq!(collection.rules.len(), 2);
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let ev1 = json!({"CommandLine": "net user admin"});
let m1 = engine.evaluate(&Event::from_value(&ev1));
assert_eq!(m1.len(), 1);
assert_eq!(m1[0].rule_title, "Detect Net User");
let ev2 = json!({"CommandLine": "net group admins"});
let m2 = engine.evaluate(&Event::from_value(&ev2));
assert_eq!(m2.len(), 1);
assert_eq!(m2[0].rule_title, "Detect Net Group");
}
#[test]
fn test_neq_modifier_yaml() {
let yaml = r#"
title: Non-Standard Port
logsource:
product: windows
detection:
selection:
Protocol: TCP
filter:
DestinationPort|neq: 443
condition: selection and filter
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"Protocol": "TCP", "DestinationPort": "80"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"Protocol": "TCP", "DestinationPort": "443"});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_neq_modifier_integer() {
let yaml = r#"
title: Non-Standard Port Numeric
logsource:
product: windows
detection:
selection:
DestinationPort|neq: 443
condition: selection
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"DestinationPort": 80});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"DestinationPort": 443});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_selector_them_excludes_underscore() {
let yaml = r#"
title: Underscore Test
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'whoami'
_helper:
User: 'SYSTEM'
condition: all of them
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"CommandLine": "whoami", "User": "admin"});
assert_eq!(
engine.evaluate(&Event::from_value(&ev)).len(),
1,
"all of them should exclude _helper, so only selection is required"
);
}
#[test]
fn test_selector_them_includes_non_underscore() {
let yaml = r#"
title: Multiple Selections
logsource:
product: windows
detection:
sel_cmd:
CommandLine|contains: 'cmd'
sel_ps:
CommandLine|contains: 'powershell'
_private:
User: 'admin'
condition: 1 of them
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"CommandLine": "cmd.exe", "User": "guest"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"CommandLine": "notepad", "User": "admin"});
assert!(
engine.evaluate(&Event::from_value(&ev2)).is_empty(),
"_private should be excluded from 'them'"
);
}
#[test]
fn test_utf16le_modifier_yaml() {
let yaml = r#"
title: Wide String
logsource:
product: windows
detection:
selection:
Payload|wide|base64: 'Test'
condition: selection
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"Payload": "VABlAHMAdAA="});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
}
#[test]
fn test_utf16be_modifier_yaml() {
let yaml = r#"
title: UTF16BE String
logsource:
product: windows
detection:
selection:
Payload|utf16be|base64: 'AB'
condition: selection
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"Payload": "AEEAQg=="});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
}
#[test]
fn test_utf16_bom_modifier_yaml() {
let yaml = r#"
title: UTF16 BOM String
logsource:
product: windows
detection:
selection:
Payload|utf16|base64: 'A'
condition: selection
level: medium
"#;
let engine = make_engine_with_rule(yaml);
let ev = json!({"Payload": "//5BAA=="});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
}
#[test]
fn test_pipeline_field_mapping_e2e() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Sysmon to ECS
transformations:
- type: field_name_mapping
mapping:
CommandLine: process.command_line
rule_conditions:
- type: logsource
product: windows
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Detect Whoami
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: medium
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev = json!({"process.command_line": "cmd /c whoami"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"CommandLine": "cmd /c whoami"});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_pipeline_add_condition_e2e() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Add index condition
transformations:
- type: add_condition
conditions:
source: windows
rule_conditions:
- type: logsource
product: windows
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Detect Cmd
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'cmd'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev = json!({"CommandLine": "cmd.exe", "source": "windows"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ev2 = json!({"CommandLine": "cmd.exe"});
assert!(engine.evaluate(&Event::from_value(&ev2)).is_empty());
}
#[test]
fn test_pipeline_change_logsource_e2e() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Change logsource
transformations:
- type: change_logsource
product: elastic
category: endpoint
rule_conditions:
- type: logsource
product: windows
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Test Rule
logsource:
product: windows
category: process_creation
detection:
selection:
action: test
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev = json!({"action": "test"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let ls = LogSource {
product: Some("windows".to_string()),
category: Some("process_creation".to_string()),
..Default::default()
};
assert!(
engine
.evaluate_with_logsource(&Event::from_value(&ev), &ls)
.is_empty(),
"logsource was changed; windows/process_creation should not match"
);
let ls2 = LogSource {
product: Some("elastic".to_string()),
category: Some("endpoint".to_string()),
..Default::default()
};
assert_eq!(
engine
.evaluate_with_logsource(&Event::from_value(&ev), &ls2)
.len(),
1,
"elastic/endpoint should match the transformed logsource"
);
}
#[test]
fn test_pipeline_replace_string_e2e() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Replace backslash
transformations:
- type: replace_string
regex: "\\\\"
replacement: "/"
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Path Detection
logsource:
product: windows
detection:
selection:
FilePath|contains: 'C:\Windows'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev = json!({"FilePath": "C:/Windows/System32/cmd.exe"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
}
#[test]
fn test_pipeline_skips_non_matching_rules() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Windows Only
transformations:
- type: field_name_prefix
prefix: "win."
rule_conditions:
- type: logsource
product: windows
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Windows Rule
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: low
---
title: Linux Rule
logsource:
product: linux
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
assert_eq!(collection.rules.len(), 2);
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev_win = json!({"win.CommandLine": "whoami"});
let m = engine.evaluate(&Event::from_value(&ev_win));
assert_eq!(m.len(), 1);
assert_eq!(m[0].rule_title, "Windows Rule");
let ev_linux = json!({"CommandLine": "whoami"});
let m2 = engine.evaluate(&Event::from_value(&ev_linux));
assert_eq!(m2.len(), 1);
assert_eq!(m2[0].rule_title, "Linux Rule");
}
#[test]
fn test_multiple_pipelines_e2e() {
use crate::pipeline::parse_pipeline;
let p1_yaml = r#"
name: First Pipeline
priority: 10
transformations:
- type: field_name_mapping
mapping:
CommandLine: process.args
"#;
let p2_yaml = r#"
name: Second Pipeline
priority: 20
transformations:
- type: field_name_suffix
suffix: ".keyword"
"#;
let p1 = parse_pipeline(p1_yaml).unwrap();
let p2 = parse_pipeline(p2_yaml).unwrap();
let rule_yaml = r#"
title: Test
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'test'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new();
engine.add_pipeline(p1);
engine.add_pipeline(p2);
engine.add_collection(&collection).unwrap();
let ev = json!({"process.args.keyword": "testing"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
}
#[test]
fn test_pipeline_drop_detection_item_e2e() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Drop EventID
transformations:
- type: drop_detection_item
field_name_conditions:
- type: include_fields
fields:
- EventID
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Sysmon Process
logsource:
product: windows
detection:
selection:
EventID: 1
CommandLine|contains: 'whoami'
condition: selection
level: medium
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev = json!({"CommandLine": "whoami"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
let mut engine2 = Engine::new();
engine2.add_collection(&collection).unwrap();
assert!(engine2.evaluate(&Event::from_value(&ev)).is_empty());
}
#[test]
fn test_pipeline_set_state_and_conditional() {
use crate::pipeline::parse_pipeline;
let pipeline_yaml = r#"
name: Stateful Pipeline
transformations:
- id: mark_windows
type: set_state
key: is_windows
value: "true"
rule_conditions:
- type: logsource
product: windows
- type: field_name_prefix
prefix: "winlog."
rule_conditions:
- type: processing_state
key: is_windows
val: "true"
"#;
let pipeline = parse_pipeline(pipeline_yaml).unwrap();
let rule_yaml = r#"
title: Windows Detect
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'test'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(rule_yaml).unwrap();
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).unwrap();
let ev = json!({"winlog.CommandLine": "testing"});
assert_eq!(engine.evaluate(&Event::from_value(&ev)).len(), 1);
}
#[test]
fn test_evaluate_batch_matches_sequential() {
let yaml = r#"
title: Login
logsource:
product: windows
detection:
selection:
EventType: 'login'
condition: selection
---
title: Process Create
logsource:
product: windows
detection:
selection:
EventType: 'process_create'
condition: selection
---
title: Keyword
logsource:
product: windows
detection:
selection:
CommandLine|contains: 'whoami'
condition: selection
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = Engine::new();
engine.add_collection(&collection).unwrap();
let vals = [
json!({"EventType": "login", "User": "admin"}),
json!({"EventType": "process_create", "CommandLine": "whoami"}),
json!({"EventType": "file_create"}),
json!({"CommandLine": "whoami /all"}),
];
let events: Vec<Event> = vals.iter().map(Event::from_value).collect();
let sequential: Vec<Vec<_>> = events.iter().map(|e| engine.evaluate(e)).collect();
let refs: Vec<&Event> = events.iter().collect();
let batch = engine.evaluate_batch(&refs);
assert_eq!(sequential.len(), batch.len());
for (seq, bat) in sequential.iter().zip(batch.iter()) {
assert_eq!(seq.len(), bat.len());
for (s, b) in seq.iter().zip(bat.iter()) {
assert_eq!(s.rule_title, b.rule_title);
}
}
}
}