mod array;
mod from_ir;
mod helpers;
#[doc(hidden)]
pub mod optimizer;
#[cfg(test)]
mod tests;
mod value;
pub(crate) use array::{
array_quantifier_from_member_matches, array_quantifier_matches_empty, decisive_member_verdict,
element_field, eval_array_body, eval_array_item, eval_array_quantified,
select_recorded_member_indices,
};
pub use from_ir::compile_to_compiled;
#[cfg(test)]
pub(crate) use optimizer::optimize_any_of as optimize_any_of_for_test;
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(test)]
use rsigma_parser::DetectionItem;
use rsigma_parser::{
ArrayQuantifier, ConditionExpr, Detection, Level, LogSource, Quantifier, SigmaRule,
};
use crate::error::Result;
use crate::event::{Event, EventValue};
use crate::matcher::CompiledMatcher;
use crate::result::{
DetectionBody, EvaluationResult, FieldMatch, MatchDetailLevel, MatcherKind, ResultBody,
RuleHeader,
};
pub(crate) use helpers::{yaml_to_json, yaml_to_json_map};
#[derive(Debug, Clone)]
pub struct CompiledRule {
pub title: String,
pub id: Option<String>,
pub name: Option<String>,
pub level: Option<Level>,
pub tags: Vec<String>,
pub description: Option<String>,
pub falsepositives: Vec<String>,
pub logsource: LogSource,
pub detections: HashMap<String, CompiledDetection>,
pub conditions: Vec<ConditionExpr>,
pub include_event: bool,
pub custom_attributes: Arc<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone)]
pub enum CompiledDetection {
AllOf(Vec<CompiledDetectionItem>),
AnyOf(Vec<CompiledDetection>),
Keywords(CompiledMatcher),
ArrayMatch {
field: String,
quantifier: ArrayQuantifier,
body: Box<CompiledDetection>,
},
And(Vec<CompiledDetection>),
Conditional {
named: HashMap<String, CompiledDetection>,
condition: ConditionExpr,
},
}
#[derive(Debug, Clone)]
pub struct CompiledDetectionItem {
pub field: Option<String>,
pub matcher: CompiledMatcher,
pub exists: Option<bool>,
pub bloom_eligible: bool,
}
pub fn compile_rule(rule: &SigmaRule) -> Result<CompiledRule> {
let ir = rsigma_ir::lower_rule(rule, &rsigma_ir::LowerOptions::default())?;
compile_to_compiled(&ir)
}
pub fn evaluate_rule(rule: &CompiledRule, event: &impl Event) -> Option<EvaluationResult> {
evaluate_rule_with_bloom(
rule,
event,
&crate::engine::bloom_index::NoBloom,
MatchDetailLevel::Off,
)
}
pub(crate) fn evaluate_rule_with_bloom<E, B>(
rule: &CompiledRule,
event: &E,
bloom: &B,
level: MatchDetailLevel,
) -> Option<EvaluationResult>
where
E: Event,
B: crate::engine::bloom_index::BloomLookup,
{
for condition in &rule.conditions {
if eval_condition_matches_with_bloom(condition, &rule.detections, event, bloom) {
let mut matched_selections = Vec::new();
let matched = eval_condition_with_bloom(
condition,
&rule.detections,
event,
&mut matched_selections,
bloom,
);
debug_assert!(matched, "detail pass must agree with boolean pass");
let matched_fields =
collect_field_matches(&matched_selections, &rule.detections, event, level);
let event_data = if rule.include_event {
Some(event.to_json())
} else {
None
};
return Some(EvaluationResult {
header: RuleHeader {
rule_title: rule.title.clone(),
rule_id: rule.id.clone(),
level: rule.level,
tags: rule.tags.clone(),
custom_attributes: rule.custom_attributes.clone(),
enrichments: None,
},
body: ResultBody::Detection(DetectionBody {
matched_selections,
matched_fields,
event: event_data,
}),
});
}
}
None
}
pub fn compile_detection(detection: &Detection) -> Result<CompiledDetection> {
let opts = rsigma_ir::LowerOptions {
permissive_placeholders: true,
};
from_ir::compile_ir_detection(&rsigma_ir::lower::lower_detection(detection, &opts)?)
}
#[cfg(test)]
fn compile_detection_item(item: &DetectionItem) -> Result<CompiledDetectionItem> {
let ir = rsigma_ir::lower::lower_detection_item(item, &rsigma_ir::LowerOptions::default())?;
from_ir::compile_ir_detection_item(&ir)
}
pub fn eval_condition(
expr: &ConditionExpr,
detections: &HashMap<String, CompiledDetection>,
event: &impl Event,
matched_selections: &mut Vec<String>,
) -> bool {
eval_condition_with_bloom(
expr,
detections,
event,
matched_selections,
&crate::engine::bloom_index::NoBloom,
)
}
fn eval_condition_matches_with_bloom<E, B>(
expr: &ConditionExpr,
detections: &HashMap<String, CompiledDetection>,
event: &E,
bloom: &B,
) -> bool
where
E: Event,
B: crate::engine::bloom_index::BloomLookup,
{
match expr {
ConditionExpr::Identifier(name) => detections
.get(name)
.is_some_and(|det| eval_detection_with_bloom(det, event, bloom)),
ConditionExpr::And(exprs) => exprs
.iter()
.all(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
ConditionExpr::Or(exprs) => exprs
.iter()
.any(|e| eval_condition_matches_with_bloom(e, detections, event, bloom)),
ConditionExpr::Not(inner) => {
!eval_condition_matches_with_bloom(inner, detections, event, bloom)
}
ConditionExpr::Selector {
quantifier,
pattern,
} => {
let mut matching = detections
.iter()
.filter(|(name, _)| pattern.matches_detection_name(name));
match quantifier {
Quantifier::Any => {
matching.any(|(_, det)| eval_detection_with_bloom(det, event, bloom))
}
Quantifier::All => {
matching.all(|(_, det)| eval_detection_with_bloom(det, event, bloom))
}
Quantifier::Count(required) => {
if *required == 0 {
return true;
}
let mut matched = 0u64;
matching.any(|(_, det)| {
if eval_detection_with_bloom(det, event, bloom) {
matched += 1;
}
matched >= *required
})
}
}
}
}
}
pub(crate) fn eval_condition_with_bloom<E, B>(
expr: &ConditionExpr,
detections: &HashMap<String, CompiledDetection>,
event: &E,
matched_selections: &mut Vec<String>,
bloom: &B,
) -> bool
where
E: Event,
B: crate::engine::bloom_index::BloomLookup,
{
match expr {
ConditionExpr::Identifier(name) => {
if let Some(det) = detections.get(name) {
let result = eval_detection_with_bloom(det, event, bloom);
if result {
matched_selections.push(name.clone());
}
result
} else {
false
}
}
ConditionExpr::And(exprs) => exprs
.iter()
.all(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
ConditionExpr::Or(exprs) => exprs
.iter()
.any(|e| eval_condition_with_bloom(e, detections, event, matched_selections, bloom)),
ConditionExpr::Not(inner) => {
!eval_condition_with_bloom(inner, detections, event, matched_selections, bloom)
}
ConditionExpr::Selector {
quantifier,
pattern,
} => {
let matching_names: Vec<&String> = detections
.keys()
.filter(|name| pattern.matches_detection_name(name))
.collect();
let mut match_count = 0u64;
for name in &matching_names {
if let Some(det) = detections.get(*name)
&& eval_detection_with_bloom(det, event, bloom)
{
match_count += 1;
matched_selections.push((*name).clone());
}
}
match quantifier {
Quantifier::Any => match_count >= 1,
Quantifier::All => match_count == matching_names.len() as u64,
Quantifier::Count(n) => match_count >= *n,
}
}
}
}
#[cfg(test)]
fn eval_detection_item(item: &CompiledDetectionItem, event: &impl Event) -> bool {
eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
}
pub(crate) fn eval_detection_item_no_bloom(
item: &CompiledDetectionItem,
event: &impl Event,
) -> bool {
eval_detection_item_with_bloom(item, event, &crate::engine::bloom_index::NoBloom)
}
fn eval_detection_with_bloom<E, B>(detection: &CompiledDetection, event: &E, bloom: &B) -> bool
where
E: Event,
B: crate::engine::bloom_index::BloomLookup,
{
match detection {
CompiledDetection::AllOf(items) => items
.iter()
.all(|item| eval_detection_item_with_bloom(item, event, bloom)),
CompiledDetection::AnyOf(dets) => dets
.iter()
.any(|d| eval_detection_with_bloom(d, event, bloom)),
CompiledDetection::Keywords(matcher) => matcher.matches_keyword(event),
CompiledDetection::ArrayMatch {
field,
quantifier,
body,
} => match event.get_field(field) {
Some(value) => eval_array_quantified(&value, *quantifier, body, event),
None => array_quantifier_matches_empty(*quantifier),
},
CompiledDetection::And(dets) => dets
.iter()
.all(|d| eval_detection_with_bloom(d, event, bloom)),
CompiledDetection::Conditional { named, condition } => {
eval_condition_with_bloom(condition, named, event, &mut Vec::new(), bloom)
}
}
}
fn eval_detection_item_with_bloom<E, B>(item: &CompiledDetectionItem, event: &E, bloom: &B) -> bool
where
E: Event,
B: crate::engine::bloom_index::BloomLookup,
{
if let Some(expect_exists) = item.exists {
if let Some(field) = &item.field {
return event.get_field(field).is_some() == expect_exists;
}
return !expect_exists;
}
match &item.field {
Some(field_name) => {
if let Some(value) = event.get_field(field_name) {
if item.bloom_eligible
&& bloom.verdict_for_field(field_name)
== crate::engine::bloom_index::BloomVerdict::DefinitelyNoMatch
{
return false;
}
item.matcher.matches(&value, event)
} else {
item.matcher.matches_absent()
}
}
None => item.matcher.matches_keyword(event),
}
}
const MAX_KEYWORD_MATCHES: usize = 16;
fn collect_field_matches(
selection_names: &[String],
detections: &HashMap<String, CompiledDetection>,
event: &impl Event,
level: MatchDetailLevel,
) -> Vec<FieldMatch> {
let mut matches = Vec::new();
for name in selection_names {
if let Some(det) = detections.get(name) {
collect_detection_fields(name, det, event, level, &mut matches);
}
}
matches
}
fn collect_detection_fields(
selection: &str,
detection: &CompiledDetection,
event: &impl Event,
level: MatchDetailLevel,
out: &mut Vec<FieldMatch>,
) {
match detection {
CompiledDetection::AllOf(items) => {
for item in items {
match &item.field {
Some(field_name) => {
if let Some(value) = event.get_field(field_name) {
if item.matcher.matches(&value, event) {
out.push(make_field_match(
selection,
field_name,
value.to_json(),
&item.matcher,
level,
));
}
} else if level != MatchDetailLevel::Off
&& (item.exists == Some(false) || item.matcher.matches_absent())
{
out.push(make_field_match(
selection,
field_name,
serde_json::Value::Null,
&item.matcher,
level,
));
}
}
None => {
if level != MatchDetailLevel::Off {
collect_keyword_matches(selection, &item.matcher, event, level, out);
}
}
}
}
}
CompiledDetection::AnyOf(dets) => {
for d in dets {
if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
collect_detection_fields(selection, d, event, level, out);
}
}
}
CompiledDetection::ArrayMatch { field, body, .. } => {
let value = event.get_field(field);
collect_array_match_fields(
selection,
field,
body,
value.as_ref(),
event,
level,
"",
out,
);
}
CompiledDetection::And(dets) => {
for d in dets {
if eval_detection_with_bloom(d, event, &crate::engine::bloom_index::NoBloom) {
collect_detection_fields(selection, d, event, level, out);
}
}
}
CompiledDetection::Conditional { .. } => {}
CompiledDetection::Keywords(matcher) => {
if level != MatchDetailLevel::Off {
collect_keyword_matches(selection, matcher, event, level, out);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn collect_array_match_fields<E: Event>(
selection: &str,
field: &str,
body: &CompiledDetection,
value: Option<&EventValue>,
outer: &E,
level: MatchDetailLevel,
path_prefix: &str,
out: &mut Vec<FieldMatch>,
) {
let container_path = if path_prefix.is_empty() {
field.to_string()
} else {
array::join_member_field(path_prefix, Some(field))
};
let (scalar, members): (bool, Vec<&EventValue>) = match value {
None => return,
Some(EventValue::Null) => {
out.push(FieldMatch::new(container_path, serde_json::Value::Null));
return;
}
Some(EventValue::Array(items)) if items.is_empty() => {
out.push(FieldMatch::new(
container_path,
serde_json::Value::Array(Vec::new()),
));
return;
}
Some(EventValue::Array(items)) => (false, items.iter().collect()),
Some(single) => (true, vec![single]),
};
let matching: Vec<usize> = members
.iter()
.enumerate()
.filter(|(_, m)| array::eval_array_body(body, m, outer))
.map(|(i, _)| i)
.take(array::ARRAY_MEMBER_CAP)
.collect();
if matching.is_empty() {
if let Some(v) = value {
out.push(FieldMatch::new(container_path, v.to_json()));
}
return;
}
for i in matching {
let member_path = array::array_member_path(&container_path, i, scalar);
let before = out.len();
collect_array_body_fields(selection, body, members[i], outer, level, &member_path, out);
if out.len() == before && level != MatchDetailLevel::Off {
out.push(FieldMatch::new(member_path, members[i].to_json()));
}
}
}
fn collect_array_body_fields<E: Event>(
selection: &str,
body: &CompiledDetection,
member: &EventValue,
outer: &E,
level: MatchDetailLevel,
member_path: &str,
out: &mut Vec<FieldMatch>,
) {
match body {
CompiledDetection::AllOf(items) => {
for item in items {
if !array::eval_array_item(item, member, outer) {
continue;
}
let relative = item.field.as_deref();
let absent =
relative.is_some_and(|name| array::element_field(member, name).is_none());
if absent && level == MatchDetailLevel::Off {
continue;
}
let path = array::join_member_field(member_path, relative);
let value = match relative {
Some(name) => array::element_field(member, name)
.map(|v| v.to_json())
.unwrap_or(serde_json::Value::Null),
None => member.to_json(),
};
out.push(make_field_match(
selection,
&path,
value,
&item.matcher,
level,
));
}
}
CompiledDetection::AnyOf(dets) | CompiledDetection::And(dets) => {
for d in dets {
if array::eval_array_body(d, member, outer) {
collect_array_body_fields(selection, d, member, outer, level, member_path, out);
}
}
}
CompiledDetection::ArrayMatch {
field, body: inner, ..
} => {
collect_array_match_fields(
selection,
field,
inner,
array::element_field(member, field),
outer,
level,
member_path,
out,
);
}
CompiledDetection::Keywords(matcher) => {
if level != MatchDetailLevel::Off && matcher.matches(member, outer) {
let d = matcher.describe();
out.push(FieldMatch {
field: member_path.to_string(),
value: member.to_json(),
selection: Some(selection.to_string()),
matcher: Some(MatcherKind::Keyword),
pattern: if level == MatchDetailLevel::Full {
d.pattern
} else {
None
},
case_sensitive: d.case_sensitive,
negated: d.negated,
});
}
}
CompiledDetection::Conditional { named, condition } => {
collect_array_condition_fields(
selection,
condition,
named,
member,
outer,
level,
member_path,
out,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn collect_array_condition_fields<E: Event>(
selection: &str,
expr: &ConditionExpr,
named: &HashMap<String, CompiledDetection>,
member: &EventValue,
outer: &E,
level: MatchDetailLevel,
member_path: &str,
out: &mut Vec<FieldMatch>,
) {
match expr {
ConditionExpr::Identifier(name) => {
if let Some(det) = named.get(name)
&& array::eval_array_body(det, member, outer)
{
collect_array_body_fields(selection, det, member, outer, level, member_path, out);
}
}
ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
for e in exprs {
collect_array_condition_fields(
selection,
e,
named,
member,
outer,
level,
member_path,
out,
);
}
}
ConditionExpr::Not(_) => {}
ConditionExpr::Selector { pattern, .. } => {
let mut names: Vec<&String> = named
.keys()
.filter(|n| pattern.matches_detection_name(n))
.collect();
names.sort();
for name in names {
if let Some(det) = named.get(name)
&& array::eval_array_body(det, member, outer)
{
collect_array_body_fields(
selection,
det,
member,
outer,
level,
member_path,
out,
);
}
}
}
}
}
fn make_field_match(
selection: &str,
field: &str,
value: serde_json::Value,
matcher: &CompiledMatcher,
level: MatchDetailLevel,
) -> FieldMatch {
match level {
MatchDetailLevel::Off => FieldMatch::new(field, value),
MatchDetailLevel::Summary | MatchDetailLevel::Full => {
let d = matcher.describe();
FieldMatch {
field: field.to_string(),
value,
selection: Some(selection.to_string()),
matcher: Some(d.kind),
pattern: if level == MatchDetailLevel::Full {
d.pattern
} else {
None
},
case_sensitive: d.case_sensitive,
negated: d.negated,
}
}
}
}
fn collect_keyword_matches(
selection: &str,
matcher: &CompiledMatcher,
event: &impl Event,
level: MatchDetailLevel,
out: &mut Vec<FieldMatch>,
) {
match matcher {
CompiledMatcher::AllOf(children)
| CompiledMatcher::CaseInsensitiveGroup {
children,
mode: crate::matcher::GroupMode::All,
} => {
for child in children {
collect_keyword_matches(selection, child, event, level, out);
}
return;
}
CompiledMatcher::Not(_) => return,
_ => {}
}
let descriptor = matcher.describe();
let mut count = 0;
for s in event.all_string_values() {
if count >= MAX_KEYWORD_MATCHES {
break;
}
if matcher.matches_str(&s) {
count += 1;
out.push(FieldMatch {
field: "keyword".to_string(),
value: serde_json::Value::String(s.into_owned()),
selection: Some(selection.to_string()),
matcher: Some(MatcherKind::Keyword),
pattern: if level == MatchDetailLevel::Full {
descriptor.pattern.clone()
} else {
None
},
case_sensitive: descriptor.case_sensitive,
negated: descriptor.negated,
});
}
}
}