pub mod builtin;
pub mod conditions;
pub mod finalizers;
mod parsing;
pub mod sources;
pub mod state;
pub mod transformations;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use rsigma_parser::{CorrelationRule, SigmaCollection, SigmaRule};
use crate::error::{EvalError, Result};
pub use conditions::{
ConditionOp, ConditionSet, DetectionItemCondition, FieldNameCondition,
NamedDetectionItemCondition, NamedFieldNameCondition, NamedRuleCondition, RuleCondition,
StateOperator, ValueMatch, eval_condition_expr,
};
pub use finalizers::Finalizer;
pub use parsing::{
parse_pipeline, parse_pipeline_file, parse_sources, parse_sources_dir, parse_sources_file,
parse_transformation_items, validate_source_refs,
};
pub use state::PipelineState;
pub use transformations::Transformation;
#[derive(Debug, Clone)]
pub struct Pipeline {
pub name: String,
pub priority: i32,
pub vars: HashMap<String, Vec<String>>,
pub transformations: Vec<TransformationItem>,
pub finalizers: Vec<Finalizer>,
pub source_refs: Vec<sources::SourceRef>,
}
#[derive(Debug, Clone)]
pub struct TransformationItem {
pub id: Option<String>,
pub transformation: Transformation,
pub rule_conditions: ConditionSet<RuleCondition>,
pub detection_item_conditions: ConditionSet<DetectionItemCondition>,
pub field_name_conditions: ConditionSet<FieldNameCondition>,
}
impl Pipeline {
pub fn apply(&self, rule: &mut SigmaRule, state: &mut PipelineState) -> Result<()> {
state.reset_rule();
state.track_detection_items = uses_detection_item_applied(&self.transformations);
for item in &self.transformations {
if !self.check_rule_conditions(rule, state, item) {
continue;
}
state.current_item_id = item.id.clone();
let applied = item.apply_tracked(rule, state);
state.current_item_id = None;
if applied? && let Some(ref id) = item.id {
state.mark_applied(id);
}
}
Ok(())
}
pub fn apply_to_collection(&self, collection: &SigmaCollection) -> Result<Vec<SigmaRule>> {
let mut state = PipelineState::new(self.vars.clone());
let mut transformed = Vec::with_capacity(collection.rules.len());
for rule in &collection.rules {
let mut cloned = rule.clone();
self.apply(&mut cloned, &mut state)?;
transformed.push(cloned);
}
Ok(transformed)
}
fn check_rule_conditions(
&self,
rule: &SigmaRule,
state: &PipelineState,
item: &TransformationItem,
) -> bool {
item.rule_conditions.conditions.is_empty()
|| item
.rule_conditions
.matches(|condition| condition.matches_rule(rule, state))
}
pub fn apply_to_correlation(
&self,
corr: &mut CorrelationRule,
state: &mut PipelineState,
) -> Result<()> {
state.reset_rule();
apply_correlation_items(corr, &self.transformations, state)
}
pub fn is_dynamic(&self) -> bool {
!self.source_refs.is_empty()
}
pub fn dynamic_references(&self) -> &[sources::SourceRef] {
&self.source_refs
}
}
fn uses_detection_item_applied(items: &[TransformationItem]) -> bool {
items.iter().any(|item| {
item.detection_item_conditions
.conditions
.iter()
.any(|named| {
matches!(
named.condition,
DetectionItemCondition::ProcessingItemApplied { .. }
)
})
|| matches!(
&item.transformation,
Transformation::Nest { items } if uses_detection_item_applied(items)
)
})
}
fn apply_correlation_items(
corr: &mut CorrelationRule,
items: &[TransformationItem],
state: &mut PipelineState,
) -> Result<()> {
for item in items {
let rule_ok = item.rule_conditions.conditions.is_empty()
|| item
.rule_conditions
.matches(|condition| condition.matches_correlation(corr, state));
if !rule_ok {
continue;
}
let outer_id = std::mem::replace(&mut state.current_item_id, item.id.clone());
let applied = apply_correlation_transformation(
corr,
&item.transformation,
state,
&item.field_name_conditions,
);
state.current_item_id = outer_id;
if applied? && let Some(ref id) = item.id {
state.mark_applied(id);
}
}
Ok(())
}
fn apply_correlation_transformation(
corr: &mut CorrelationRule,
transformation: &Transformation,
state: &mut PipelineState,
field_name_conditions: &ConditionSet<FieldNameCondition>,
) -> Result<bool> {
match transformation {
Transformation::FieldNameMapping { mapping } => {
map_correlation_fields(corr, state, field_name_conditions, |name| {
mapping.get(name).cloned()
})?;
Ok(true)
}
Transformation::FieldNamePrefixMapping { mapping } => {
map_correlation_fields(corr, state, field_name_conditions, |name| {
mapping.iter().find_map(|(prefix, replacement)| {
name.strip_prefix(prefix.as_str())
.map(|rest| vec![format!("{replacement}{rest}")])
})
})?;
Ok(true)
}
Transformation::FieldNamePrefix { prefix } => {
map_correlation_fields(corr, state, field_name_conditions, |name| {
Some(vec![format!("{prefix}{name}")])
})?;
Ok(true)
}
Transformation::FieldNameSuffix { suffix } => {
map_correlation_fields(corr, state, field_name_conditions, |name| {
Some(vec![format!("{name}{suffix}")])
})?;
Ok(true)
}
Transformation::FieldNameTransform {
transform_func,
mapping,
} => {
map_correlation_fields(corr, state, field_name_conditions, |name| {
Some(vec![mapping.get(name).cloned().unwrap_or_else(|| {
transformations::apply_named_string_fn(transform_func, name)
})])
})?;
Ok(true)
}
Transformation::SetCustomAttribute { attribute, value } => {
corr.custom_attributes
.insert(attribute.clone(), yaml_serde::Value::String(value.clone()));
Ok(true)
}
Transformation::SetState { key, value } => {
state.set_state(key.clone(), value.clone());
Ok(true)
}
Transformation::RuleFailure { message } => Err(EvalError::InvalidModifiers(format!(
"Pipeline rule failure: {message} (correlation: {})",
corr.title
))),
Transformation::Nest { items } => {
apply_correlation_items(corr, items, state)?;
Ok(true)
}
_ => Ok(false),
}
}
fn map_correlation_fields(
corr: &mut CorrelationRule,
state: &mut PipelineState,
field_name_conditions: &ConditionSet<FieldNameCondition>,
mapper: impl Fn(&str) -> Option<Vec<String>>,
) -> Result<()> {
let mut renames = Vec::new();
let current: &PipelineState = state;
let rename = |name: &str| {
mapper(name).filter(|names| {
!names.is_empty()
&& field_name_conditions
.matches(|condition| condition.matches_field_name(name, current))
})
};
corr.fields = transformations::rename_field_list(
std::mem::take(&mut corr.fields),
current,
&[field_name_conditions],
&mapper,
&mut renames,
);
let alias_names: std::collections::HashSet<String> =
corr.aliases.iter().map(|a| a.alias.clone()).collect();
for alias in &mut corr.aliases {
for (rule_ref, field_name) in &mut alias.mapping {
let Some(names) = rename(field_name) else {
continue;
};
if names.len() > 1 {
return Err(EvalError::InvalidModifiers(format!(
"field_name_mapping one-to-many cannot be applied to \
correlation alias mapping (alias '{}', rule '{}', \
field '{}' maps to {} alternatives)",
alias.alias,
rule_ref,
field_name,
names.len(),
)));
}
let renamed = names[0].clone();
renames.push((std::mem::replace(field_name, renamed), names));
}
}
let mut group_by = Vec::with_capacity(corr.group_by.len());
for field_name in std::mem::take(&mut corr.group_by) {
if alias_names.contains(&field_name) {
group_by.push(field_name);
continue;
}
let Some(names) = rename(&field_name) else {
group_by.push(field_name);
continue;
};
if names.len() > 1 {
log::warn!(
"correlation '{}': group_by field '{}' has a one-to-many \
mapping ({} alternatives: {:?}); expanding all, so \
correlation grouping may be broader than intended",
corr.title,
field_name,
names.len(),
names,
);
}
group_by.extend(names.iter().cloned());
renames.push((field_name, names));
}
corr.group_by = group_by;
if let rsigma_parser::CorrelationCondition::Threshold { ref mut field, .. } = corr.condition
&& let Some(fields) = field.as_mut()
{
for f in fields.iter_mut() {
let Some(names) = rename(f) else {
continue;
};
if names.len() > 1 {
return Err(EvalError::InvalidModifiers(format!(
"field_name_mapping one-to-many cannot be applied to \
correlation condition field reference ('{}' maps to \
{} alternatives)",
f,
names.len(),
)));
}
let renamed = names[0].clone();
renames.push((std::mem::replace(f, renamed), names));
}
}
state.track_field_renames(renames);
Ok(())
}
pub fn merge_pipelines(pipelines: &mut [Pipeline]) {
pipelines.sort_by_key(|p| p.priority);
}
pub fn apply_pipelines(pipelines: &[Pipeline], rule: &mut SigmaRule) -> Result<()> {
for pipeline in pipelines {
let mut state = PipelineState::new(pipeline.vars.clone());
pipeline.apply(rule, &mut state)?;
}
Ok(())
}
pub fn apply_pipelines_with_state(
pipelines: &[Pipeline],
rule: &mut SigmaRule,
) -> Result<PipelineState> {
let mut merged = PipelineState::default();
for pipeline in pipelines {
let mut state = PipelineState::new(pipeline.vars.clone());
pipeline.apply(rule, &mut state)?;
for (k, v) in state.state {
merged.state.insert(k, v);
}
merged.applied_items.extend(state.applied_items);
merged.vars.extend(state.vars);
}
Ok(merged)
}
#[derive(Debug, Clone)]
pub struct TransformedRule {
pub rule: SigmaRule,
pub applied_items: Vec<String>,
pub state: PipelineState,
}
pub fn transform_rule(pipelines: &[Pipeline], rule: &SigmaRule) -> Result<TransformedRule> {
let mut transformed = rule.clone();
let state = apply_pipelines_with_state(pipelines, &mut transformed)?;
let mut applied_items: Vec<String> = state.applied_items.iter().cloned().collect();
applied_items.sort();
Ok(TransformedRule {
rule: transformed,
applied_items,
state,
})
}
pub fn transform_collection(
pipelines: &[Pipeline],
collection: &SigmaCollection,
) -> Result<Vec<TransformedRule>> {
collection
.rules
.iter()
.map(|rule| transform_rule(pipelines, rule))
.collect()
}
pub fn apply_pipelines_to_correlation(
pipelines: &[Pipeline],
corr: &mut CorrelationRule,
) -> Result<PipelineState> {
let mut merged = PipelineState::default();
for pipeline in pipelines {
let mut state = PipelineState::new(pipeline.vars.clone());
pipeline.apply_to_correlation(corr, &mut state)?;
for (k, v) in state.state {
merged.state.insert(k, v);
}
merged.applied_items.extend(state.applied_items);
merged.vars.extend(state.vars);
}
Ok(merged)
}