mod helpers;
mod mod_ctx;
mod value;
use std::collections::HashMap;
use rsigma_parser::{
ConditionExpr, CorrelationRule, Detection, DetectionItem, Detections, FilterRule, SigmaRule,
SigmaValue,
};
use crate::error::IrError;
use crate::{
IrCondition, IrCorrelation, IrDetection, IrDetectionItem, IrFilter, IrMatcher, IrRule,
IrRuleMetadata,
};
use helpers::{Result, yaml_to_json_map};
use mod_ctx::{ModCtx, validate_modifiers};
use value::{lower_value, lower_value_keywords};
#[derive(Debug, Clone, Default)]
pub struct LowerOptions {
pub permissive_placeholders: bool,
}
pub fn lower_rule(rule: &SigmaRule, opts: &LowerOptions) -> Result<IrRule> {
let mut detections = HashMap::new();
for (name, detection) in &rule.detection.named {
detections.insert(name.clone(), lower_detection(detection, opts)?);
}
let detection_names: Vec<String> = detections.keys().cloned().collect();
let mut conditions = Vec::with_capacity(rule.detection.conditions.len());
for condition in &rule.detection.conditions {
conditions.push(lower_condition(condition, &detection_names)?);
}
Ok(IrRule {
metadata: metadata_from_rule(rule),
logsource: rule.logsource.clone(),
sigma_version: rule.sigma_version,
detections,
conditions,
})
}
pub fn lower_conditions(section: &Detections) -> Result<Vec<IrCondition>> {
let names: Vec<String> = section.named.keys().cloned().collect();
section
.conditions
.iter()
.map(|c| lower_condition(c, &names))
.collect()
}
pub fn lower_detection(detection: &Detection, opts: &LowerOptions) -> Result<IrDetection> {
match detection {
Detection::AllOf(items) => {
if items.is_empty() {
return Err(IrError::InvalidModifiers(
"AllOf detection must not be empty (vacuous truth)".into(),
));
}
let lowered: Result<Vec<_>> = items
.iter()
.map(|item| lower_detection_item(item, opts))
.collect();
Ok(IrDetection::AllOf(lowered?))
}
Detection::AnyOf(dets) => {
if dets.is_empty() {
return Err(IrError::InvalidModifiers(
"AnyOf detection must not be empty (would never match)".into(),
));
}
let lowered: Result<Vec<_>> = dets.iter().map(|d| lower_detection(d, opts)).collect();
Ok(IrDetection::AnyOf(lowered?))
}
Detection::ArrayMatch {
field,
quantifier,
body,
} => {
let compiled_body = lower_detection(body, opts)?;
Ok(IrDetection::ArrayMatch {
field: field.clone(),
quantifier: *quantifier,
body: Box::new(compiled_body),
})
}
Detection::And(dets) => {
if dets.is_empty() {
return Err(IrError::InvalidModifiers(
"And detection must not be empty".into(),
));
}
let lowered: Result<Vec<_>> = dets.iter().map(|d| lower_detection(d, opts)).collect();
Ok(IrDetection::And(lowered?))
}
Detection::Conditional { named, condition } => {
if named.is_empty() {
return Err(IrError::InvalidModifiers(
"Conditional detection must have at least one named sub-selection".into(),
));
}
let mut lowered_named = HashMap::new();
for (k, d) in named {
lowered_named.insert(k.clone(), lower_detection(d, opts)?);
}
let names: Vec<String> = lowered_named.keys().cloned().collect();
let lowered_cond = lower_condition(condition, &names)?;
Ok(IrDetection::Conditional {
named: lowered_named,
condition: lowered_cond,
})
}
Detection::Keywords(values) => {
let matchers: Result<Vec<IrMatcher>> =
values.iter().map(lower_value_keywords).collect();
let matchers = matchers?;
let matcher = match matchers.len() {
0 => {
return Err(IrError::InvalidModifiers(
"Keywords detection must not be empty".into(),
));
}
1 => matchers.into_iter().next().unwrap(),
_ => IrMatcher::AnyOf(matchers),
};
Ok(IrDetection::Keywords(matcher))
}
}
}
pub fn lower_detection_item(item: &DetectionItem, opts: &LowerOptions) -> Result<IrDetectionItem> {
if !opts.permissive_placeholders {
for v in &item.values {
reject_placeholders(v)?;
}
}
let ctx = ModCtx::from_modifiers(&item.field.modifiers);
validate_modifiers(&ctx, &item.field.modifiers)?;
if ctx.exists {
let expect = match item.values.first() {
Some(SigmaValue::Bool(b)) => *b,
Some(SigmaValue::String(s)) => match s.as_plain().as_deref() {
Some("true") | Some("yes") => true,
Some("false") | Some("no") => false,
_ => true,
},
_ => true,
};
return Ok(IrDetectionItem {
field: item.field.name.clone(),
matcher: IrMatcher::Exists(expect),
exists: Some(expect),
});
}
if ctx.all && item.values.len() <= 1 {
return Err(IrError::InvalidModifiers(
"|all modifier requires more than one value".to_string(),
));
}
let matchers: Result<Vec<IrMatcher>> =
item.values.iter().map(|v| lower_value(v, &ctx)).collect();
let matchers = matchers?;
let combined = if ctx.all {
if matchers.len() == 1 {
matchers.into_iter().next().unwrap()
} else {
IrMatcher::AllOf(matchers)
}
} else if matchers.len() == 1 {
matchers.into_iter().next().unwrap()
} else {
IrMatcher::AnyOf(matchers)
};
Ok(IrDetectionItem {
field: item.field.name.clone(),
matcher: combined,
exists: None,
})
}
pub fn lower_condition(expr: &ConditionExpr, detection_names: &[String]) -> Result<IrCondition> {
match expr {
ConditionExpr::Identifier(name) => {
if !detection_names.iter().any(|n| n == name) {
return Err(IrError::UnknownDetection(name.clone()));
}
Ok(IrCondition::Detection(name.clone()))
}
ConditionExpr::And(exprs) => {
let lowered: Result<Vec<_>> = exprs
.iter()
.map(|e| lower_condition(e, detection_names))
.collect();
Ok(IrCondition::And(lowered?))
}
ConditionExpr::Or(exprs) => {
let lowered: Result<Vec<_>> = exprs
.iter()
.map(|e| lower_condition(e, detection_names))
.collect();
Ok(IrCondition::Or(lowered?))
}
ConditionExpr::Not(inner) => Ok(IrCondition::Not(Box::new(lower_condition(
inner,
detection_names,
)?))),
ConditionExpr::Selector {
quantifier,
pattern,
} => {
Ok(IrCondition::Selector {
quantifier: quantifier.clone(),
pattern: pattern.clone(),
})
}
}
}
pub fn lower_correlation(corr: &CorrelationRule) -> Result<IrCorrelation> {
Ok(IrCorrelation {
metadata: metadata_from_correlation(corr),
sigma_version: corr.sigma_version,
correlation_type: corr.correlation_type,
rules: corr.rules.clone(),
group_by: corr.group_by.clone(),
timespan: corr.timespan.clone(),
window: corr.window,
gap: corr.gap.clone(),
condition: corr.condition.clone(),
aliases: corr.aliases.clone(),
generate: corr.generate,
})
}
pub fn lower_filter(filter: &FilterRule, opts: &LowerOptions) -> Result<IrFilter> {
let mut detections = HashMap::new();
for (name, detection) in &filter.detection.named {
detections.insert(name.clone(), lower_detection(detection, opts)?);
}
let conditions = lower_conditions(&filter.detection)?;
Ok(IrFilter {
metadata: metadata_from_filter(filter),
sigma_version: filter.sigma_version,
rules: filter.rules.clone(),
logsource: filter.logsource.clone(),
detections,
conditions,
})
}
fn reject_placeholders(value: &SigmaValue) -> Result<()> {
if let SigmaValue::String(s) = value
&& s.original.contains("${source.")
{
return Err(IrError::Lowering(format!(
"unresolved source placeholder in detection value: {}",
s.original
)));
}
Ok(())
}
fn metadata_from_rule(rule: &SigmaRule) -> IrRuleMetadata {
IrRuleMetadata {
title: rule.title.clone(),
id: rule.id.clone(),
name: rule.name.clone(),
level: rule.level,
tags: rule.tags.clone(),
status: rule.status,
description: rule.description.clone(),
author: rule.author.clone(),
date: rule.date.clone(),
modified: rule.modified.clone(),
references: rule.references.clone(),
falsepositives: rule.falsepositives.clone(),
fields: rule.fields.clone(),
related: rule.related.clone(),
license: rule.license.clone(),
taxonomy: rule.taxonomy.clone(),
scope: rule.scope.clone(),
custom_attributes: yaml_to_json_map(&rule.custom_attributes),
schema_affinity: None,
}
}
fn metadata_from_correlation(corr: &CorrelationRule) -> IrRuleMetadata {
IrRuleMetadata {
title: corr.title.clone(),
id: corr.id.clone(),
name: corr.name.clone(),
level: corr.level,
tags: corr.tags.clone(),
status: corr.status,
description: corr.description.clone(),
author: corr.author.clone(),
date: corr.date.clone(),
modified: corr.modified.clone(),
references: corr.references.clone(),
falsepositives: corr.falsepositives.clone(),
fields: corr.fields.clone(),
related: corr.related.clone(),
license: corr.license.clone(),
taxonomy: corr.taxonomy.clone(),
scope: corr.scope.clone(),
custom_attributes: yaml_to_json_map(&corr.custom_attributes),
schema_affinity: None,
}
}
fn metadata_from_filter(filter: &FilterRule) -> IrRuleMetadata {
IrRuleMetadata {
title: filter.title.clone(),
id: filter.id.clone(),
name: filter.name.clone(),
level: filter.level,
tags: filter.tags.clone(),
status: filter.status,
description: filter.description.clone(),
author: filter.author.clone(),
date: filter.date.clone(),
modified: filter.modified.clone(),
references: filter.references.clone(),
falsepositives: filter.falsepositives.clone(),
fields: filter.fields.clone(),
related: filter.related.clone(),
license: filter.license.clone(),
taxonomy: filter.taxonomy.clone(),
scope: filter.scope.clone(),
custom_attributes: yaml_to_json_map(&filter.custom_attributes),
schema_affinity: None,
}
}