use std::collections::HashMap;
use regex::Regex;
use rsigma_parser::{
ConditionExpr, Detection, DetectionItem, FieldSpec, SigmaRule, SigmaString, SigmaValue,
SpecialChar, StringPart,
};
use super::super::conditions::{DetectionItemCondition, FieldNameCondition};
use super::super::state::PipelineState;
use crate::error::{EvalError, Result};
const MAX_FIELD_MAPPING_COMBINATIONS: usize = 4096;
pub(super) fn apply_field_name_transform<F>(
rule: &mut SigmaRule,
state: &PipelineState,
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
transform_fn: F,
) -> Result<()>
where
F: Fn(&str) -> Option<Vec<String>>,
{
let rule_title = rule.title.clone();
for detection in rule.detection.named.values_mut() {
transform_detection_fields(
detection,
state,
field_name_conditions,
field_name_cond_not,
&transform_fn,
&rule_title,
)?;
}
Ok(())
}
fn transform_detection_fields<F>(
detection: &mut Detection,
state: &PipelineState,
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
transform_fn: &F,
rule_title: &str,
) -> Result<()>
where
F: Fn(&str) -> Option<Vec<String>>,
{
match detection {
Detection::AllOf(items) => {
enum Resolved {
Unchanged,
Renamed(String),
Expanded(Vec<String>),
}
let resolved: Vec<Resolved> = items
.iter()
.map(|item| match item.field.name.as_deref() {
Some(name)
if field_conditions_match(
name,
state,
field_name_conditions,
field_name_cond_not,
) =>
{
match transform_fn(name) {
Some(new_names) if new_names.len() > 1 => Resolved::Expanded(new_names),
Some(mut new_names) if new_names.len() == 1 => {
Resolved::Renamed(new_names.pop().unwrap())
}
_ => Resolved::Unchanged,
}
}
_ => Resolved::Unchanged,
})
.collect();
let needs_expansion = resolved.iter().any(|r| matches!(r, Resolved::Expanded(_)));
if !needs_expansion {
for (item, res) in items.iter_mut().zip(resolved) {
if let Resolved::Renamed(new_name) = res {
item.field.name = Some(new_name);
}
}
} else {
let alternatives: Vec<Vec<DetectionItem>> = items
.iter()
.zip(resolved)
.map(|(item, res)| match res {
Resolved::Expanded(names) => names
.into_iter()
.map(|new_name| {
let mut clone = item.clone();
clone.field.name = Some(new_name);
clone
})
.collect(),
Resolved::Renamed(name) => {
let mut clone = item.clone();
clone.field.name = Some(name);
vec![clone]
}
Resolved::Unchanged => vec![item.clone()],
})
.collect();
let total = alternatives
.iter()
.map(Vec::len)
.fold(1usize, |acc, n| acc.saturating_mul(n));
if total > MAX_FIELD_MAPPING_COMBINATIONS {
let sizes: Vec<usize> = alternatives.iter().map(Vec::len).collect();
return Err(EvalError::InvalidModifiers(format!(
"field name mapping cartesian expansion would produce {total} \
branches, exceeding the limit of {MAX_FIELD_MAPPING_COMBINATIONS} \
(rule: {rule_title}, per-item alternative counts: {sizes:?}); \
reduce the number of one-to-many alternatives or split the AllOf"
)));
}
let combinations = cartesian_product(alternatives);
*detection =
Detection::AnyOf(combinations.into_iter().map(Detection::AllOf).collect());
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
transform_detection_fields(
sub,
state,
field_name_conditions,
field_name_cond_not,
transform_fn,
rule_title,
)?;
}
}
Detection::Keywords(_) => {}
}
Ok(())
}
fn cartesian_product<T: Clone>(input: Vec<Vec<T>>) -> Vec<Vec<T>> {
let mut result: Vec<Vec<T>> = vec![Vec::new()];
for group in input {
let mut next = Vec::with_capacity(result.len() * group.len().max(1));
for prefix in &result {
for elem in &group {
let mut combo = prefix.clone();
combo.push(elem.clone());
next.push(combo);
}
}
result = next;
}
result
}
fn field_conditions_match(
field_name: &str,
state: &PipelineState,
conditions: &[FieldNameCondition],
negate: bool,
) -> bool {
if conditions.is_empty() {
return true;
}
let all_match = conditions
.iter()
.all(|c| c.matches_field_name(field_name, state));
if negate { !all_match } else { all_match }
}
pub(super) fn drop_detection_items(
rule: &mut SigmaRule,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
) {
for detection in rule.detection.named.values_mut() {
drop_from_detection(
detection,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
);
}
}
fn drop_from_detection(
detection: &mut Detection,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
) {
match detection {
Detection::AllOf(items) => {
items.retain(|item| {
!should_drop_item(
item,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
)
});
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
drop_from_detection(
sub,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
);
}
}
Detection::Keywords(_) => {}
}
}
fn should_drop_item(
item: &DetectionItem,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
) -> bool {
let det_match = detection_conditions.is_empty()
|| detection_conditions
.iter()
.all(|c| c.matches_item(item, state));
let field_match = if let Some(ref name) = item.field.name {
field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
} else {
field_name_conditions.is_empty()
};
det_match && field_match
}
pub(super) fn add_conditions(
rule: &mut SigmaRule,
conditions: &HashMap<String, SigmaValue>,
negated: bool,
) {
let items: Vec<DetectionItem> = conditions
.iter()
.map(|(field, value)| DetectionItem {
field: FieldSpec::new(Some(field.clone()), Vec::new()),
values: vec![value.clone()],
})
.collect();
let det_name = format!("__pipeline_cond_{}", rule.detection.named.len());
rule.detection
.named
.insert(det_name.clone(), Detection::AllOf(items));
let cond_ref = ConditionExpr::Identifier(det_name);
let cond_expr = if negated {
ConditionExpr::Not(Box::new(cond_ref))
} else {
cond_ref
};
rule.detection.conditions = rule
.detection
.conditions
.iter()
.map(|existing| ConditionExpr::And(vec![existing.clone(), cond_expr.clone()]))
.collect();
}
#[allow(clippy::too_many_arguments)]
pub(super) fn replace_strings_in_rule(
rule: &mut SigmaRule,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
re: &Regex,
replacement: &str,
skip_special: bool,
) {
for detection in rule.detection.named.values_mut() {
replace_strings_in_detection(
detection,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
re,
replacement,
skip_special,
);
}
}
#[allow(clippy::too_many_arguments)]
fn replace_strings_in_detection(
detection: &mut Detection,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
re: &Regex,
replacement: &str,
skip_special: bool,
) {
match detection {
Detection::AllOf(items) => {
for item in items.iter_mut() {
let det_match = detection_conditions.is_empty()
|| detection_conditions
.iter()
.all(|c| c.matches_item(item, state));
let field_match = if let Some(ref name) = item.field.name {
field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
} else {
field_name_conditions.is_empty()
};
if det_match && field_match {
replace_strings_in_values(&mut item.values, re, replacement, skip_special);
}
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
replace_strings_in_detection(
sub,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
re,
replacement,
skip_special,
);
}
}
Detection::Keywords(values) => {
replace_strings_in_values(values, re, replacement, skip_special);
}
}
}
fn replace_strings_in_values(
values: &mut [SigmaValue],
re: &Regex,
replacement: &str,
skip_special: bool,
) {
for value in values.iter_mut() {
if let SigmaValue::String(s) = value {
if skip_special && s.contains_wildcards() {
let new_parts: Vec<StringPart> = s
.parts
.iter()
.map(|part| match part {
StringPart::Plain(text) => {
let replaced = re.replace_all(text, replacement);
StringPart::Plain(replaced.into_owned())
}
special => special.clone(),
})
.collect();
if new_parts != s.parts {
let new_original = parts_to_original(&new_parts);
s.parts = new_parts;
s.original = new_original;
}
} else {
let replaced = re.replace_all(&s.original, replacement);
if replaced != s.original {
*s = SigmaString::new(&replaced);
}
}
}
}
}
fn parts_to_original(parts: &[StringPart]) -> String {
let mut out = String::new();
for part in parts {
match part {
StringPart::Plain(text) => {
for c in text.chars() {
if c == '*' || c == '?' || c == '\\' {
out.push('\\');
}
out.push(c);
}
}
StringPart::Special(SpecialChar::WildcardMulti) => out.push('*'),
StringPart::Special(SpecialChar::WildcardSingle) => out.push('?'),
}
}
out
}
pub(super) fn expand_placeholders_in_rule(
rule: &mut SigmaRule,
state: &PipelineState,
wildcard: bool,
) {
for detection in rule.detection.named.values_mut() {
expand_placeholders_in_detection(detection, state, wildcard);
}
}
fn expand_placeholders_in_detection(
detection: &mut Detection,
state: &PipelineState,
wildcard: bool,
) {
match detection {
Detection::AllOf(items) => {
for item in items.iter_mut() {
expand_placeholders_in_values(&mut item.values, state, wildcard);
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
expand_placeholders_in_detection(sub, state, wildcard);
}
}
Detection::Keywords(values) => {
expand_placeholders_in_values(values, state, wildcard);
}
}
}
fn expand_placeholders_in_values(
values: &mut Vec<SigmaValue>,
state: &PipelineState,
wildcard: bool,
) {
let mut expanded_values = Vec::new();
for value in values.drain(..) {
if let SigmaValue::String(ref s) = value {
let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
if plain.contains('%') {
let result = expand_placeholder_string(&plain, state, wildcard);
expanded_values.extend(result);
continue;
}
}
expanded_values.push(value);
}
*values = expanded_values;
}
fn expand_placeholder_string(s: &str, state: &PipelineState, wildcard: bool) -> Vec<SigmaValue> {
let mut result = s.to_string();
while let Some(start) = result.find('%') {
let rest = &result[start + 1..];
let Some(end) = rest.find('%') else {
break;
};
let placeholder = &rest[..end];
if let Some(values) = state.vars.get(placeholder) {
if values.len() == 1 {
result = format!("{}{}{}", &result[..start], values[0], &rest[end + 1..]);
} else if values.is_empty() {
if wildcard {
result = format!("{}*{}", &result[..start], &rest[end + 1..]);
} else {
break;
}
} else {
return values
.iter()
.map(|v| {
let expanded = format!("{}{}{}", &result[..start], v, &rest[end + 1..]);
SigmaValue::String(SigmaString::new(&expanded))
})
.collect();
}
} else if wildcard {
result = format!("{}*{}", &result[..start], &rest[end + 1..]);
} else {
break;
}
}
vec![SigmaValue::String(SigmaString::new(&result))]
}
pub(super) fn apply_named_string_fn(func: &str, s: &str) -> String {
match func {
"lower" | "lowercase" => s.to_lowercase(),
"upper" | "uppercase" => s.to_uppercase(),
"title" => s
.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + &c.as_str().to_lowercase(),
}
})
.collect::<Vec<_>>()
.join("_"),
"snake_case" => {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
out.push('_');
}
out.push(ch.to_lowercase().next().unwrap_or(ch));
}
out
}
_ => s.to_string(),
}
}
pub(super) fn decompose_hashes_field(
rule: &mut SigmaRule,
valid_algos: &[String],
field_prefix: &str,
drop_algo_prefix: bool,
) {
for detection in rule.detection.named.values_mut() {
decompose_hashes_in_detection(detection, valid_algos, field_prefix, drop_algo_prefix);
}
}
fn decompose_hashes_in_detection(
detection: &mut Detection,
valid_algos: &[String],
field_prefix: &str,
drop_algo_prefix: bool,
) {
match detection {
Detection::AllOf(items) => {
let mut new_items: Vec<DetectionItem> = Vec::new();
let mut i = 0;
while i < items.len() {
let item = &items[i];
let is_hashes = item
.field
.name
.as_deref()
.map(|n| n.eq_ignore_ascii_case("hashes"))
.unwrap_or(false);
if is_hashes {
for val in &item.values {
if let SigmaValue::String(s) = val {
let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
for pair in plain.split(',') {
let pair = pair.trim();
if let Some((algo, hash)) = pair.split_once('=') {
let algo_upper = algo.trim().to_uppercase();
if valid_algos.is_empty()
|| valid_algos
.iter()
.any(|a| a.eq_ignore_ascii_case(&algo_upper))
{
let field_name = if drop_algo_prefix {
field_prefix.to_string()
} else {
format!("{field_prefix}{}", algo.trim())
};
new_items.push(DetectionItem {
field: FieldSpec::new(
Some(field_name),
item.field.modifiers.clone(),
),
values: vec![SigmaValue::String(SigmaString::new(
hash.trim(),
))],
});
}
}
}
}
}
} else {
new_items.push(items[i].clone());
}
i += 1;
}
*items = new_items;
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
decompose_hashes_in_detection(sub, valid_algos, field_prefix, drop_algo_prefix);
}
}
Detection::Keywords(_) => {}
}
}
pub(super) fn map_string_values(
rule: &mut SigmaRule,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
mapping: &HashMap<String, Vec<String>>,
) {
for detection in rule.detection.named.values_mut() {
map_strings_in_detection(
detection,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
mapping,
);
}
}
fn map_strings_in_detection(
detection: &mut Detection,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
mapping: &HashMap<String, Vec<String>>,
) {
match detection {
Detection::AllOf(items) => {
for item in items.iter_mut() {
if item_conditions_match(
item,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
) {
map_string_expand_values(&mut item.values, mapping);
}
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
map_strings_in_detection(
sub,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
mapping,
);
}
}
Detection::Keywords(values) => {
map_string_expand_values(values, mapping);
}
}
}
fn map_string_expand_values(values: &mut Vec<SigmaValue>, mapping: &HashMap<String, Vec<String>>) {
let mut extra: Vec<(usize, Vec<SigmaValue>)> = Vec::new();
for (i, val) in values.iter_mut().enumerate() {
if let SigmaValue::String(s) = val {
let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
if let Some(replacements) = mapping.get(&plain) {
if let Some(first) = replacements.first() {
*s = SigmaString::new(first);
}
if replacements.len() > 1 {
let extras: Vec<SigmaValue> = replacements[1..]
.iter()
.map(|r| SigmaValue::String(SigmaString::new(r)))
.collect();
extra.push((i, extras));
}
}
}
}
for (idx, extras) in extra.into_iter().rev() {
for (j, v) in extras.into_iter().enumerate() {
values.insert(idx + 1 + j, v);
}
}
}
pub(super) fn set_detection_item_values(
rule: &mut SigmaRule,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
value: &SigmaValue,
) {
for detection in rule.detection.named.values_mut() {
set_values_in_detection(
detection,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
value,
);
}
}
fn set_values_in_detection(
detection: &mut Detection,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
value: &SigmaValue,
) {
match detection {
Detection::AllOf(items) => {
for item in items.iter_mut() {
if item_conditions_match(
item,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
) {
item.values = vec![value.clone()];
}
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
set_values_in_detection(
sub,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
value,
);
}
}
Detection::Keywords(_) => {}
}
}
pub(super) fn convert_detection_item_types(
rule: &mut SigmaRule,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
target_type: &str,
) {
for detection in rule.detection.named.values_mut() {
convert_types_in_detection(
detection,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
target_type,
);
}
}
fn convert_types_in_detection(
detection: &mut Detection,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
target_type: &str,
) {
match detection {
Detection::AllOf(items) => {
for item in items.iter_mut() {
if item_conditions_match(
item,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
) {
for val in item.values.iter_mut() {
*val = convert_value(val, target_type);
}
}
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
convert_types_in_detection(
sub,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
target_type,
);
}
}
Detection::Keywords(_) => {}
}
}
fn convert_value(val: &SigmaValue, target: &str) -> SigmaValue {
match target {
"str" | "string" => match val {
SigmaValue::String(_) => val.clone(),
SigmaValue::Integer(n) => SigmaValue::String(SigmaString::new(&n.to_string())),
SigmaValue::Float(f) => SigmaValue::String(SigmaString::new(&f.to_string())),
SigmaValue::Bool(b) => SigmaValue::String(SigmaString::new(&b.to_string())),
SigmaValue::Null => SigmaValue::String(SigmaString::new("null")),
},
"int" | "integer" => match val {
SigmaValue::String(s) => {
let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
plain
.parse::<i64>()
.map(SigmaValue::Integer)
.unwrap_or_else(|_| val.clone())
}
SigmaValue::Float(f) => SigmaValue::Integer(*f as i64),
SigmaValue::Bool(b) => SigmaValue::Integer(if *b { 1 } else { 0 }),
_ => val.clone(),
},
"float" => match val {
SigmaValue::String(s) => {
let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
plain
.parse::<f64>()
.map(SigmaValue::Float)
.unwrap_or_else(|_| val.clone())
}
SigmaValue::Integer(n) => SigmaValue::Float(*n as f64),
SigmaValue::Bool(b) => SigmaValue::Float(if *b { 1.0 } else { 0.0 }),
_ => val.clone(),
},
"bool" | "boolean" => match val {
SigmaValue::String(s) => {
let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
match plain.to_lowercase().as_str() {
"true" | "1" | "yes" => SigmaValue::Bool(true),
"false" | "0" | "no" => SigmaValue::Bool(false),
_ => val.clone(),
}
}
SigmaValue::Integer(n) => SigmaValue::Bool(*n != 0),
SigmaValue::Float(f) => SigmaValue::Bool(*f != 0.0),
_ => val.clone(),
},
_ => val.clone(),
}
}
pub(super) fn apply_case_transformation(
rule: &mut SigmaRule,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
case_type: &str,
) {
for detection in rule.detection.named.values_mut() {
apply_case_in_detection(
detection,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
case_type,
);
}
}
fn apply_case_in_detection(
detection: &mut Detection,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
case_type: &str,
) {
match detection {
Detection::AllOf(items) => {
for item in items.iter_mut() {
if item_conditions_match(
item,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
) {
for val in item.values.iter_mut() {
apply_case_to_value(val, case_type);
}
}
}
}
Detection::AnyOf(subs) => {
for sub in subs.iter_mut() {
apply_case_in_detection(
sub,
state,
detection_conditions,
field_name_conditions,
field_name_cond_not,
case_type,
);
}
}
Detection::Keywords(values) => {
for val in values.iter_mut() {
apply_case_to_value(val, case_type);
}
}
}
}
fn apply_case_to_value(val: &mut SigmaValue, case_type: &str) {
if let SigmaValue::String(s) = val {
let transformed = match case_type {
"lower" | "lowercase" => s.original.to_lowercase(),
"upper" | "uppercase" => s.original.to_uppercase(),
"snake_case" => apply_named_string_fn("snake_case", &s.original),
_ => return,
};
if transformed != s.original {
*s = SigmaString::new(&transformed);
}
}
}
fn item_conditions_match(
item: &DetectionItem,
state: &PipelineState,
detection_conditions: &[DetectionItemCondition],
field_name_conditions: &[FieldNameCondition],
field_name_cond_not: bool,
) -> bool {
let det_match = detection_conditions.is_empty()
|| detection_conditions
.iter()
.all(|c| c.matches_item(item, state));
let field_match = if let Some(ref name) = item.field.name {
field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
} else {
field_name_conditions.is_empty()
};
det_match && field_match
}
pub(super) fn rule_has_matching_item(
rule: &SigmaRule,
state: &PipelineState,
conditions: &[DetectionItemCondition],
) -> bool {
for detection in rule.detection.named.values() {
if detection_has_matching_item(detection, state, conditions) {
return true;
}
}
false
}
fn detection_has_matching_item(
detection: &Detection,
state: &PipelineState,
conditions: &[DetectionItemCondition],
) -> bool {
match detection {
Detection::AllOf(items) => items
.iter()
.any(|item| conditions.iter().all(|c| c.matches_item(item, state))),
Detection::AnyOf(subs) => subs
.iter()
.any(|sub| detection_has_matching_item(sub, state, conditions)),
Detection::Keywords(_) => false,
}
}