use rsigma_eval::{Engine, JsonEvent, MatchResult, parse_pipeline};
use rsigma_parser::parse_sigma_yaml;
use serde_json::{Value, json};
fn engine_from(yaml: &str) -> Engine {
let collection = parse_sigma_yaml(yaml).expect("rules parse");
let mut engine = Engine::new();
engine.add_collection(&collection).expect("compile");
engine
}
fn sorted_titles(results: Vec<MatchResult>) -> Vec<String> {
let mut titles: Vec<String> = results.into_iter().map(|m| m.rule_title).collect();
titles.sort();
titles
}
fn evaluate_corpus(engine: &Engine, events: &[Value]) -> Vec<Vec<String>> {
events
.iter()
.map(|ev| {
let event = JsonEvent::borrow(ev);
sorted_titles(engine.evaluate(&event))
})
.collect()
}
const CONTAINS_HEAVY_RULES: &str = r#"
title: Suspicious LOLBAS
id: lolbas-bench
logsource:
product: windows
category: process_creation
detection:
selection:
Image|contains:
- 'whoami'
- 'mimikatz'
- 'invoke-mimikatz'
- 'powershell'
- 'rundll32'
- 'regsvr32'
- 'certutil'
- 'bitsadmin'
- 'mshta'
- 'wscript'
condition: selection
level: high
---
title: Process Create Login
id: login-event
logsource:
product: windows
category: process_creation
detection:
selection:
EventType: 'login'
condition: selection
level: low
---
title: Mixed Modifiers
id: mixed-mods
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains:
- '-encodedcommand'
- '-enc'
- 'frombase64string'
- 'iex'
- 'invoke-expression'
- 'downloadstring'
Image|endswith: '.exe'
condition: selection
level: medium
"#;
const ALL_OF_CONTAINS_RULES: &str = r#"
title: AllOf Contains Sentinel
id: allof-contains
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains|all:
- 'powershell'
- '-enc'
- 'http'
condition: selection
level: medium
"#;
#[test]
fn baseline_contains_heavy_corpus() {
let engine = engine_from(CONTAINS_HEAVY_RULES);
let events = vec![
json!({"EventType": "login", "Image": "C:/Windows/System32/explorer.exe"}),
json!({"Image": "C:/Tools/MIMIKATZ.exe", "CommandLine": "mimikatz.exe sekurlsa::logonpasswords"}),
json!({"Image": "C:/Windows/System32/powershell.exe", "CommandLine": "powershell.exe -enc aHR0cHM6Ly9ldmls"}),
json!({"Image": "/usr/bin/whoami", "CommandLine": "whoami /all"}),
json!({"Image": "/usr/bin/notepad.exe", "CommandLine": "notepad readme.txt"}),
json!({}),
];
let actual = evaluate_corpus(&engine, &events);
let expected: Vec<Vec<String>> = vec![
vec!["Process Create Login".into()],
vec!["Suspicious LOLBAS".into()],
vec!["Mixed Modifiers".into(), "Suspicious LOLBAS".into()],
vec!["Suspicious LOLBAS".into()],
Vec::<String>::new(),
Vec::<String>::new(),
];
assert_eq!(actual, expected, "regression: optimizer changed results");
}
#[test]
fn allof_contains_semantics_preserved() {
let engine = engine_from(ALL_OF_CONTAINS_RULES);
let events = vec![
json!({"CommandLine": "powershell.exe -enc http://evil.com/x"}),
json!({"CommandLine": "powershell.exe -enc dummy"}),
json!({"CommandLine": "powershell.exe foo"}),
json!({"CommandLine": "notepad.exe"}),
];
let actual = evaluate_corpus(&engine, &events);
let expected: Vec<Vec<String>> = vec![
vec!["AllOf Contains Sentinel".into()],
Vec::<String>::new(),
Vec::<String>::new(),
Vec::<String>::new(),
];
assert_eq!(
actual, expected,
"AllOf|contains semantics broken: rule fired on partial match"
);
}
#[test]
fn keyword_aho_corasick_path_correct() {
let yaml = r#"
title: Keyword AC Path
id: keyword-ac
logsource:
product: windows
detection:
keywords:
- 'whoami'
- 'MIMIKATZ'
- 'invoke-expression'
- 'powershell'
- 'rundll32'
- 'regsvr32'
- 'certutil'
- 'bitsadmin'
- 'mshta.exe'
condition: keywords
"#;
let engine = engine_from(yaml);
let events = vec![
json!({"some_field": "oops MIMIKATZ"}),
json!({"some_field": "Invoke-Expression"}),
json!({"path": "C:/Windows/System32/CertUtil.exe"}),
json!({"path": "C:/Windows/System32/explorer.exe"}),
json!({"outer": {"inner": "powershell foo"}}),
];
let actual = evaluate_corpus(&engine, &events);
let expected: Vec<Vec<String>> = vec![
vec!["Keyword AC Path".into()],
vec!["Keyword AC Path".into()],
vec!["Keyword AC Path".into()],
Vec::<String>::new(),
vec!["Keyword AC Path".into()],
];
assert_eq!(actual, expected);
}
#[test]
fn bloom_prefilter_preserves_match_results() {
let mut engine = engine_from(CONTAINS_HEAVY_RULES);
let events = vec![
json!({"EventType": "login", "Image": "C:/Windows/System32/explorer.exe"}),
json!({"Image": "C:/Tools/MIMIKATZ.exe", "CommandLine": "mimikatz.exe sekurlsa::logonpasswords"}),
json!({"Image": "C:/Windows/System32/powershell.exe", "CommandLine": "powershell.exe -enc aHR0cHM6Ly9ldmls"}),
json!({"Image": "/usr/bin/whoami", "CommandLine": "whoami /all"}),
json!({"Image": "/usr/bin/notepad.exe", "CommandLine": "notepad readme.txt"}),
json!({"Image": "0000000000", "CommandLine": "0000111122223333"}),
json!({}),
];
engine.set_bloom_prefilter(false);
let no_bloom = evaluate_corpus(&engine, &events);
engine.set_bloom_prefilter(true);
let with_bloom = evaluate_corpus(&engine, &events);
assert_eq!(
no_bloom, with_bloom,
"bloom pre-filter changed match output"
);
}
#[test]
fn bloom_prefilter_handles_condition_negation() {
let yaml = r#"
title: Selection Without Substring
id: selection-without-substring
logsource:
product: windows
category: process_creation
detection:
selection:
EventType: 'process_create'
other:
CommandLine|contains: 'whoami'
condition: selection and not other
level: medium
"#;
let mut engine = engine_from(yaml);
engine.set_bloom_prefilter(true);
let events = vec![
json!({"EventType": "process_create", "CommandLine": "notepad foo"}),
json!({"EventType": "process_create", "CommandLine": "exec whoami"}),
json!({"EventType": "process_create", "CommandLine": "0123456789"}),
];
let actual = evaluate_corpus(&engine, &events);
let expected: Vec<Vec<String>> = vec![
vec!["Selection Without Substring".into()],
Vec::<String>::new(),
vec!["Selection Without Substring".into()],
];
assert_eq!(actual, expected);
engine.set_bloom_prefilter(false);
let no_bloom = evaluate_corpus(&engine, &events);
assert_eq!(no_bloom, expected);
}
#[test]
fn optimizer_runs_after_pipeline_transformation() {
let yaml = r#"
title: Post-Pipeline AC Check
id: post-pipeline-ac
logsource:
product: windows
category: process_creation
detection:
selection:
CommandLine|contains:
- 'whoami'
- 'mimikatz'
- 'powershell'
- 'invoke-expression'
- 'rundll32'
- 'regsvr32'
- 'certutil'
- 'bitsadmin'
condition: selection
level: high
"#;
let pipeline_yaml = r#"
name: ECS Field Mapping
priority: 10
transformations:
- type: field_name_mapping
mapping:
CommandLine: process.command_line
rule_conditions:
- type: logsource
product: windows
"#;
let collection = parse_sigma_yaml(yaml).expect("rules parse");
let pipeline = parse_pipeline(pipeline_yaml).expect("pipeline parse");
let mut engine = Engine::new_with_pipeline(pipeline);
engine.add_collection(&collection).expect("compile");
let events = vec![
json!({"process.command_line": "cmd /c whoami"}),
json!({"process.command_line": "MIMIKATZ.exe dump"}),
json!({"CommandLine": "whoami"}),
json!({"Image": "notepad.exe"}),
];
let actual = evaluate_corpus(&engine, &events);
let expected: Vec<Vec<String>> = vec![
vec!["Post-Pipeline AC Check".into()],
vec!["Post-Pipeline AC Check".into()],
Vec::<String>::new(),
Vec::<String>::new(),
];
assert_eq!(
actual, expected,
"optimizer must run after pipeline field renaming"
);
}
#[cfg(feature = "daachorse-index")]
#[test]
fn cross_rule_ac_preserves_match_results() {
let mut engine = engine_from(CONTAINS_HEAVY_RULES);
let events = vec![
json!({"EventType": "login", "Image": "C:/Windows/System32/explorer.exe"}),
json!({"Image": "C:/Tools/MIMIKATZ.exe", "CommandLine": "mimikatz.exe sekurlsa::logonpasswords"}),
json!({"Image": "C:/Windows/System32/powershell.exe", "CommandLine": "powershell.exe -enc aHR0cHM6Ly9ldmls"}),
json!({"Image": "/usr/bin/whoami", "CommandLine": "whoami /all"}),
json!({"Image": "/usr/bin/notepad.exe", "CommandLine": "notepad readme.txt"}),
json!({"Image": "0000000000", "CommandLine": "0000111122223333"}),
json!({}),
];
let no_ac = evaluate_corpus(&engine, &events);
engine.set_cross_rule_ac(true);
let with_ac = evaluate_corpus(&engine, &events);
assert_eq!(no_ac, with_ac, "cross-rule AC changed match output");
}
#[cfg(feature = "daachorse-index")]
#[test]
fn cross_rule_ac_handles_negation_in_condition() {
let yaml = r#"
title: Selection Without Substring
id: selection-without-substring
logsource:
product: windows
category: process_creation
detection:
selection:
EventType: 'process_create'
other:
CommandLine|contains: 'whoami'
condition: selection and not other
level: medium
"#;
let mut engine = engine_from(yaml);
engine.set_cross_rule_ac(true);
let events = vec![
json!({"EventType": "process_create", "CommandLine": "notepad foo"}),
json!({"EventType": "process_create", "CommandLine": "exec whoami"}),
json!({"EventType": "process_create", "CommandLine": "0123456789"}),
];
let actual = evaluate_corpus(&engine, &events);
let expected: Vec<Vec<String>> = vec![
vec!["Selection Without Substring".into()],
Vec::<String>::new(),
vec!["Selection Without Substring".into()],
];
assert_eq!(actual, expected);
}
#[cfg(feature = "daachorse-index")]
#[test]
fn cross_rule_ac_composes_with_bloom() {
let mut engine = engine_from(CONTAINS_HEAVY_RULES);
let events = vec![
json!({"EventType": "login", "Image": "C:/Windows/System32/explorer.exe"}),
json!({"Image": "C:/Tools/MIMIKATZ.exe", "CommandLine": "mimikatz.exe foo"}),
json!({"Image": "/usr/bin/whoami", "CommandLine": "whoami /all"}),
json!({"Image": "0000000000", "CommandLine": "0000111122223333"}),
];
let baseline = evaluate_corpus(&engine, &events);
engine.set_cross_rule_ac(true);
engine.set_bloom_prefilter(true);
let combined = evaluate_corpus(&engine, &events);
assert_eq!(
baseline, combined,
"cross-rule AC + bloom must agree with the no-prefilter baseline"
);
}