use rsigma_eval::{CorrelationConfig, CorrelationEngine, Engine, EvalError};
use rsigma_parser::{
Detection, SigmaRule, SigmaString, SigmaValue, parse_condition, parse_sigma_yaml,
};
fn rule_with_item(modifiers: &[&str], value: &str) -> SigmaRule {
let yaml = r#"
title: Rewritten
logsource:
product: test
detection:
selection:
Field: x
condition: selection
"#;
let mut rule = parse_sigma_yaml(yaml).unwrap().rules.remove(0);
let Some(Detection::AllOf(items)) = rule.detection.named.get_mut("selection") else {
unreachable!("the selection is a single mapping");
};
items[0].field.modifiers = modifiers.iter().map(|m| m.parse().unwrap()).collect();
items[0].values = vec![SigmaValue::String(SigmaString::new(value))];
rule
}
#[test]
fn regex_the_evaluator_cannot_run_surfaces_at_compile_time() {
let yaml = r#"
title: Lookbehind
logsource:
product: test
detection:
selection:
CommandLine|re: '(?<!\\)cmd'
condition: selection
level: low
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
assert!(collection.errors.is_empty(), "{:?}", collection.errors);
let err = Engine::new().add_collection(&collection).unwrap_err();
assert!(
matches!(err, EvalError::InvalidRegex(_)),
"expected InvalidRegex, got: {err}"
);
}
#[test]
fn invalid_regex_is_rejected_at_compile_time() {
let err = Engine::new()
.add_rule(&rule_with_item(&["re"], "[unclosed"))
.unwrap_err();
assert!(
matches!(err, EvalError::InvalidRegex(_)),
"expected InvalidRegex, got: {err}"
);
}
#[test]
fn invalid_cidr_is_rejected_at_compile_time() {
for (cidr, expected) in [
("not-a-cidr", "expected address/prefix"),
("10.1.2.3/8", "host bits set"),
("2001:db8::1/32", "host bits set"),
] {
let err = Engine::new()
.add_rule(&rule_with_item(&["cidr"], cidr))
.unwrap_err();
assert!(
matches!(err, EvalError::IncompatibleValue(_)) && err.to_string().contains(expected),
"{cidr}: {err}"
);
}
}
#[test]
fn timestamp_part_with_non_numeric_string_is_incompatible() {
let err = Engine::new()
.add_rule(&rule_with_item(&["hour"], "three"))
.unwrap_err();
assert!(
matches!(err, EvalError::IncompatibleValue(_)),
"expected IncompatibleValue, got: {err}"
);
}
#[test]
fn numeric_comparison_with_non_numeric_value() {
let err = Engine::new()
.add_rule(&rule_with_item(&["gt"], "not_a_number"))
.unwrap_err();
assert!(
matches!(err, EvalError::ExpectedNumeric(_)),
"expected ExpectedNumeric, got: {err}"
);
}
#[test]
fn unknown_detection_in_condition_errors_at_compile_time() {
let mut rule = rule_with_item(&[], "x");
rule.detection.conditions = vec![parse_condition("selection and selection_b").unwrap()];
let err = Engine::new().add_rule(&rule).unwrap_err();
assert!(
matches!(err, EvalError::UnknownDetection(ref name) if name == "selection_b"),
"expected UnknownDetection(\"selection_b\"), got: {err}"
);
}
#[test]
fn extended_condition_on_non_temporal_type() {
let yaml2 = r#"
title: Rule A
id: rule-a
logsource:
category: test
detection:
selection:
type: a
condition: selection
---
title: Rule B
id: rule-b
logsource:
category: test
detection:
selection:
type: b
condition: selection
---
title: Bad Event Count
correlation:
type: event_count
rules:
- rule-a
- rule-b
group-by:
- User
timespan: 60s
condition: rule-a and rule-b
level: high
"#;
let collection2 = parse_sigma_yaml(yaml2).unwrap();
let mut engine2 = CorrelationEngine::new(CorrelationConfig::default());
let err = engine2.add_collection(&collection2).unwrap_err();
assert!(
matches!(err, EvalError::CorrelationError(_)),
"expected CorrelationError for extended condition on event_count, got: {err}"
);
}
#[test]
fn correlation_cycle_detected_at_add_collection() {
let yaml = r#"
title: Detection
id: det-rule
logsource:
category: test
detection:
selection:
type: event
condition: selection
---
title: Corr A
id: corr-a
correlation:
type: event_count
rules:
- corr-c
group-by:
- User
timespan: 60s
condition:
gte: 2
level: high
---
title: Corr B
id: corr-b
correlation:
type: event_count
rules:
- corr-a
group-by:
- User
timespan: 60s
condition:
gte: 2
level: high
---
title: Corr C
id: corr-c
correlation:
type: event_count
rules:
- corr-b
group-by:
- User
timespan: 60s
condition:
gte: 2
level: high
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = CorrelationEngine::new(CorrelationConfig::default());
let err = engine.add_collection(&collection).unwrap_err();
assert!(
matches!(err, EvalError::CorrelationCycle(_)),
"expected CorrelationCycle, got: {err}"
);
}
#[test]
fn unknown_rule_ref_in_correlation_errors_at_add_time() {
let yaml = r#"
title: Detection
id: det-rule
logsource:
category: test
detection:
selection:
type: event
condition: selection
---
title: Orphan Correlation
correlation:
type: event_count
rules:
- nonexistent-rule-id
group-by:
- User
timespan: 60s
condition:
gte: 1
level: high
"#;
let collection = parse_sigma_yaml(yaml).unwrap();
let mut engine = CorrelationEngine::new(CorrelationConfig::default());
let err = engine.add_collection(&collection).unwrap_err();
assert!(
matches!(err, EvalError::UnknownRuleRef(ref name) if name == "nonexistent-rule-id"),
"expected UnknownRuleRef(\"nonexistent-rule-id\"), got: {err}"
);
}