use super::*;
use rsigma_parser::{
ConditionExpr, Detection, DetectionItem, Detections, FieldSpec, LogSource, Modifier,
SigmaString,
};
fn make_test_rule() -> SigmaRule {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![
DetectionItem {
field: FieldSpec::new(Some("CommandLine".to_string()), vec![Modifier::Contains]),
values: vec![SigmaValue::String(SigmaString::new("whoami"))],
},
DetectionItem {
field: FieldSpec::new(Some("ParentImage".to_string()), vec![Modifier::EndsWith]),
values: vec![SigmaValue::String(SigmaString::new("\\cmd.exe"))],
},
]),
);
SigmaRule {
title: "Test Rule".to_string(),
logsource: LogSource {
category: Some("process_creation".to_string()),
product: Some("windows".to_string()),
service: None,
definition: None,
custom: HashMap::new(),
},
detection: Detections {
named,
conditions: vec![ConditionExpr::Identifier("selection".to_string())],
condition_strings: vec!["selection".to_string()],
timeframe: None,
},
id: Some("test-001".to_string()),
name: None,
related: vec![],
taxonomy: None,
status: None,
description: None,
license: None,
author: None,
references: vec![],
date: None,
modified: None,
fields: vec![],
falsepositives: vec![],
level: Some(rsigma_parser::Level::Medium),
tags: vec![],
scope: vec![],
custom_attributes: HashMap::new(),
}
}
#[test]
fn test_field_name_mapping() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert(
"CommandLine".to_string(),
vec!["process.command_line".to_string()],
);
mapping.insert(
"ParentImage".to_string(),
vec!["process.parent.executable".to_string()],
);
let t = Transformation::FieldNameMapping { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(
items[0].field.name,
Some("process.command_line".to_string())
);
assert_eq!(
items[1].field.name,
Some("process.parent.executable".to_string())
);
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_mapping_one_to_many_expands_to_anyof() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert(
"CommandLine".to_string(),
vec!["cmd_a".to_string(), "cmd_b".to_string()],
);
let t = Transformation::FieldNameMapping { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
let Detection::AnyOf(branches) = det else {
panic!("Expected AnyOf, got {det:?}");
};
assert_eq!(branches.len(), 2);
let mut seen_first_fields: Vec<Option<String>> = Vec::new();
for branch in branches {
let Detection::AllOf(items) = branch else {
panic!("Expected AllOf in each branch, got {branch:?}");
};
assert_eq!(items.len(), 2);
assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
seen_first_fields.push(items[0].field.name.clone());
}
seen_first_fields.sort();
assert_eq!(
seen_first_fields,
vec![Some("cmd_a".to_string()), Some("cmd_b".to_string())]
);
}
#[test]
fn test_field_name_mapping_one_to_many_cartesian_when_two_items_expand() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert(
"CommandLine".to_string(),
vec!["cmd_a".to_string(), "cmd_b".to_string()],
);
mapping.insert(
"ParentImage".to_string(),
vec!["parent_x".to_string(), "parent_y".to_string()],
);
let t = Transformation::FieldNameMapping { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
let Detection::AnyOf(branches) = det else {
panic!("Expected AnyOf, got {det:?}");
};
assert_eq!(branches.len(), 4);
let mut combos: Vec<(Option<String>, Option<String>)> = branches
.iter()
.map(|b| {
let Detection::AllOf(items) = b else {
panic!("Expected AllOf");
};
(items[0].field.name.clone(), items[1].field.name.clone())
})
.collect();
combos.sort();
assert_eq!(
combos,
vec![
(Some("cmd_a".to_string()), Some("parent_x".to_string())),
(Some("cmd_a".to_string()), Some("parent_y".to_string())),
(Some("cmd_b".to_string()), Some("parent_x".to_string())),
(Some("cmd_b".to_string()), Some("parent_y".to_string())),
]
);
}
#[test]
fn test_field_name_mapping_cartesian_expansion_capped() {
use rsigma_parser::{Detection, Detections, FieldSpec, LogSource, Modifier};
let alts: Vec<String> = (0..7).map(|i| format!("alt_{i}")).collect();
let mut mapping = HashMap::new();
let mut items = Vec::new();
for i in 0..5 {
let name = format!("Field{i}");
mapping.insert(name.clone(), alts.clone());
items.push(DetectionItem {
field: FieldSpec::new(Some(name), vec![Modifier::Contains]),
values: vec![SigmaValue::String(SigmaString::new("x"))],
});
}
let mut named = HashMap::new();
named.insert("selection".to_string(), Detection::AllOf(items));
let mut rule = SigmaRule {
title: "Cartesian Bomb".to_string(),
logsource: LogSource {
category: None,
product: None,
service: None,
definition: None,
custom: HashMap::new(),
},
detection: Detections {
named,
conditions: vec![ConditionExpr::Identifier("selection".to_string())],
condition_strings: vec!["selection".to_string()],
timeframe: None,
},
id: None,
name: None,
related: vec![],
taxonomy: None,
status: None,
description: None,
license: None,
author: None,
references: vec![],
date: None,
modified: None,
fields: vec![],
falsepositives: vec![],
level: None,
tags: vec![],
scope: vec![],
custom_attributes: HashMap::new(),
};
let mut state = PipelineState::default();
let t = Transformation::FieldNameMapping { mapping };
let err = t
.apply(&mut rule, &mut state, &[], &[], false)
.expect_err("expansion above cap must error");
let msg = format!("{err}");
assert!(msg.contains("16807"), "expected total in error: {msg}");
assert!(msg.contains("4096"), "expected limit in error: {msg}");
assert!(
msg.contains("Cartesian Bomb"),
"expected rule title in error: {msg}"
);
}
#[test]
fn test_field_name_mapping_single_alternative_in_list_uses_fast_path() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert("CommandLine".to_string(), vec!["cmd".to_string()]);
let t = Transformation::FieldNameMapping { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
let Detection::AllOf(items) = det else {
panic!("Expected AllOf (no expansion), got {det:?}");
};
assert_eq!(items[0].field.name, Some("cmd".to_string()));
}
#[test]
fn test_field_name_prefix() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::FieldNamePrefix {
prefix: "winlog.event_data.".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(
items[0].field.name,
Some("winlog.event_data.CommandLine".to_string())
);
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_suffix() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::FieldNameSuffix {
suffix: ".keyword".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("CommandLine.keyword".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_change_logsource() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::ChangeLogsource {
category: Some("endpoint".to_string()),
product: Some("elastic".to_string()),
service: None,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert_eq!(rule.logsource.category, Some("endpoint".to_string()));
assert_eq!(rule.logsource.product, Some("elastic".to_string()));
}
#[test]
fn test_replace_string() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::ReplaceString {
regex: r"whoami".to_string(),
replacement: "REPLACED".to_string(),
skip_special: false,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "REPLACED");
} else {
panic!("Expected String value");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_add_condition() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut conds = HashMap::new();
conds.insert(
"index".to_string(),
SigmaValue::String(SigmaString::new("windows-*")),
);
let t = Transformation::AddCondition {
conditions: conds,
negated: false,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert!(
rule.detection
.named
.keys()
.any(|k| k.starts_with("__pipeline_cond_"))
);
assert_eq!(rule.detection.conditions.len(), 1);
if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
assert_eq!(parts.len(), 2);
} else {
panic!("Expected And condition");
}
}
#[test]
fn test_set_state() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::SetState {
key: "index".to_string(),
value: "windows".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert!(state.state_matches("index", "windows"));
}
#[test]
fn test_drop_detection_item_with_field_condition() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let field_conds = vec![FieldNameCondition::IncludeFields {
matcher: super::super::conditions::FieldMatcher::Plain(vec!["ParentImage".to_string()]),
}];
let t = Transformation::DropDetectionItem;
t.apply(&mut rule, &mut state, &[], &field_conds, false)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items.len(), 1); assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_mapping_with_conditions() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let field_conds = vec![FieldNameCondition::IncludeFields {
matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
}];
let mut mapping = HashMap::new();
mapping.insert("CommandLine".to_string(), vec!["process.args".to_string()]);
mapping.insert(
"ParentImage".to_string(),
vec!["process.parent".to_string()],
);
let t = Transformation::FieldNameMapping { mapping };
t.apply(&mut rule, &mut state, &[], &field_conds, false)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("process.args".to_string()));
assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_rule_failure() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::RuleFailure {
message: "Unsupported rule".to_string(),
};
let result = t.apply(&mut rule, &mut state, &[], &[], false);
assert!(result.is_err());
}
#[test]
fn test_value_placeholders() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("User".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("%admin_users%"))],
}]),
);
let mut rule = SigmaRule {
title: "Test".to_string(),
logsource: LogSource::default(),
detection: Detections {
named,
conditions: vec![ConditionExpr::Identifier("selection".to_string())],
condition_strings: vec!["selection".to_string()],
timeframe: None,
},
id: None,
name: None,
related: vec![],
taxonomy: None,
status: None,
description: None,
license: None,
author: None,
references: vec![],
date: None,
modified: None,
fields: vec![],
falsepositives: vec![],
level: None,
tags: vec![],
scope: vec![],
custom_attributes: HashMap::new(),
};
let mut state = PipelineState::default();
state.vars.insert(
"admin_users".to_string(),
vec!["root".to_string(), "admin".to_string()],
);
let t = Transformation::ValuePlaceholders;
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].values.len(), 2);
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_transform_lowercase() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::FieldNameTransform {
transform_func: "lower".to_string(),
mapping: HashMap::new(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("commandline".to_string()));
assert_eq!(items[1].field.name, Some("parentimage".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_transform_with_mapping_override() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert("CommandLine".to_string(), "cmd_line".to_string());
let t = Transformation::FieldNameTransform {
transform_func: "lower".to_string(),
mapping,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("cmd_line".to_string()));
assert_eq!(items[1].field.name, Some("parentimage".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_transform_snake_case() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::FieldNameTransform {
transform_func: "snake_case".to_string(),
mapping: HashMap::new(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("command_line".to_string()));
assert_eq!(items[1].field.name, Some("parent_image".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_hashes_fields_decomposition() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new(
"SHA1=abc123,MD5=def456",
))],
}]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let t = Transformation::HashesFields {
valid_hash_algos: vec!["SHA1".to_string(), "MD5".to_string()],
field_prefix: "File".to_string(),
drop_algo_prefix: false,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items.len(), 2);
assert_eq!(items[0].field.name, Some("FileSHA1".to_string()));
assert_eq!(items[1].field.name, Some("FileMD5".to_string()));
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "abc123");
}
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "def456");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_map_string() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert("whoami".to_string(), vec!["who_am_i".to_string()]);
let t = Transformation::MapString { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "who_am_i");
} else {
panic!("Expected String value");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_map_string_no_match() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert("nonexistent".to_string(), vec!["replaced".to_string()]);
let t = Transformation::MapString { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det
&& let SigmaValue::String(s) = &items[0].values[0]
{
assert_eq!(s.original, "whoami");
}
}
#[test]
fn test_set_value() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::SetValue {
value: SigmaValue::String(SigmaString::new("FIXED")),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
for item in items {
assert_eq!(item.values.len(), 1);
if let SigmaValue::String(s) = &item.values[0] {
assert_eq!(s.original, "FIXED");
}
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_convert_type_string_to_int() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("EventID".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("4688"))],
}]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let t = Transformation::ConvertType {
target_type: "int".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert!(matches!(items[0].values[0], SigmaValue::Integer(4688)));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_convert_type_int_to_string() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("EventID".to_string()), vec![]),
values: vec![SigmaValue::Integer(4688)],
}]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let t = Transformation::ConvertType {
target_type: "str".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "4688");
} else {
panic!("Expected String");
}
}
}
#[test]
fn test_convert_type_to_bool() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("Enabled".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("true"))],
}]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let t = Transformation::ConvertType {
target_type: "bool".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert!(matches!(items[0].values[0], SigmaValue::Bool(true)));
}
}
#[test]
fn test_regex_noop() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::Regex;
let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert!(!result); }
#[test]
fn test_add_field() {
let mut rule = make_test_rule();
assert!(rule.fields.is_empty());
let mut state = PipelineState::default();
let t = Transformation::AddField {
field: "EventID".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert_eq!(rule.fields, vec!["EventID".to_string()]);
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert_eq!(rule.fields, vec!["EventID".to_string()]);
}
#[test]
fn test_remove_field() {
let mut rule = make_test_rule();
rule.fields = vec!["EventID".to_string(), "CommandLine".to_string()];
let mut state = PipelineState::default();
let t = Transformation::RemoveField {
field: "EventID".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert_eq!(rule.fields, vec!["CommandLine".to_string()]);
}
#[test]
fn test_set_field() {
let mut rule = make_test_rule();
rule.fields = vec!["old".to_string()];
let mut state = PipelineState::default();
let t = Transformation::SetField {
fields: vec!["new1".to_string(), "new2".to_string()],
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert_eq!(rule.fields, vec!["new1".to_string(), "new2".to_string()]);
}
#[test]
fn test_set_custom_attribute() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::SetCustomAttribute {
attribute: "custom.key".to_string(),
value: "custom_value".to_string(),
};
let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert!(result);
assert_eq!(
rule.custom_attributes
.get("custom.key")
.and_then(|v| v.as_str()),
Some("custom_value")
);
}
#[test]
fn test_case_transformation_lower() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::CaseTransformation {
case_type: "lower".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "whoami");
}
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "\\cmd.exe");
}
}
}
#[test]
fn test_case_transformation_upper() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::CaseTransformation {
case_type: "upper".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "WHOAMI");
}
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "\\CMD.EXE");
}
}
}
#[test]
fn test_nest_transformation() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let items = vec![
super::super::TransformationItem {
id: Some("inner_prefix".to_string()),
transformation: Transformation::FieldNamePrefix {
prefix: "winlog.".to_string(),
},
rule_conditions: vec![],
rule_cond_expr: None,
detection_item_conditions: vec![],
field_name_conditions: vec![],
field_name_cond_not: false,
},
super::super::TransformationItem {
id: Some("inner_suffix".to_string()),
transformation: Transformation::FieldNameSuffix {
suffix: ".keyword".to_string(),
},
rule_conditions: vec![],
rule_cond_expr: None,
detection_item_conditions: vec![],
field_name_conditions: vec![],
field_name_cond_not: false,
},
];
let t = Transformation::Nest { items };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(
items[0].field.name,
Some("winlog.CommandLine.keyword".to_string())
);
assert_eq!(
items[1].field.name,
Some("winlog.ParentImage.keyword".to_string())
);
} else {
panic!("Expected AllOf");
}
assert!(state.was_applied("inner_prefix"));
assert!(state.was_applied("inner_suffix"));
}
#[test]
fn test_field_name_prefix_mapping() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert("Command".to_string(), "process.".to_string());
mapping.insert("Parent".to_string(), "process.parent.".to_string());
let t = Transformation::FieldNamePrefixMapping { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("process.Line".to_string()));
assert_eq!(
items[1].field.name,
Some("process.parent.Image".to_string())
);
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_prefix_mapping_no_match() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert("NoMatch".to_string(), "replaced.".to_string());
let t = Transformation::FieldNamePrefixMapping { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_wildcard_placeholders_replaces_unresolved() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("User".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("%unknown_var%"))],
}]),
);
let mut rule = SigmaRule {
title: "Test".to_string(),
logsource: LogSource::default(),
detection: Detections {
named,
conditions: vec![ConditionExpr::Identifier("selection".to_string())],
condition_strings: vec!["selection".to_string()],
timeframe: None,
},
id: None,
name: None,
related: vec![],
taxonomy: None,
status: None,
description: None,
license: None,
author: None,
references: vec![],
date: None,
modified: None,
fields: vec![],
falsepositives: vec![],
level: None,
tags: vec![],
scope: vec![],
custom_attributes: HashMap::new(),
};
let mut state = PipelineState::default();
let t = Transformation::WildcardPlaceholders;
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "*", "unresolved placeholder should become *");
} else {
panic!("Expected String value");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_wildcard_placeholders_with_known_var() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("User".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("%admin%"))],
}]),
);
let mut rule = SigmaRule {
title: "Test".to_string(),
logsource: LogSource::default(),
detection: Detections {
named,
conditions: vec![ConditionExpr::Identifier("selection".to_string())],
condition_strings: vec!["selection".to_string()],
timeframe: None,
},
id: None,
name: None,
related: vec![],
taxonomy: None,
status: None,
description: None,
license: None,
author: None,
references: vec![],
date: None,
modified: None,
fields: vec![],
falsepositives: vec![],
level: None,
tags: vec![],
scope: vec![],
custom_attributes: HashMap::new(),
};
let mut state = PipelineState::default();
state
.vars
.insert("admin".to_string(), vec!["root".to_string()]);
let t = Transformation::WildcardPlaceholders;
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "root");
} else {
panic!("Expected String value");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_detection_item_failure_fires_on_match() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let det_conds = vec![DetectionItemCondition::MatchString {
regex: regex::Regex::new("whoami").unwrap(),
negate: false,
}];
let t = Transformation::DetectionItemFailure {
message: "Unsupported detection item".to_string(),
};
let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Unsupported detection item"));
}
#[test]
fn test_detection_item_failure_skips_on_no_match() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let det_conds = vec![DetectionItemCondition::MatchString {
regex: regex::Regex::new("nonexistent_value").unwrap(),
negate: false,
}];
let t = Transformation::DetectionItemFailure {
message: "Should not fire".to_string(),
};
let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
assert!(result.is_ok());
assert!(!result.unwrap()); }
#[test]
fn test_query_expression_placeholders_stores_in_state() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::QueryExpressionPlaceholders {
expression: "{field}={value}".to_string(),
};
let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert!(result);
let stored = state.get_state("query_expression_template").unwrap();
assert_eq!(stored.as_str().unwrap(), "{field}={value}");
}
#[test]
fn test_add_condition_negated() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut conds = HashMap::new();
conds.insert(
"User".to_string(),
SigmaValue::String(SigmaString::new("SYSTEM")),
);
let t = Transformation::AddCondition {
conditions: conds,
negated: true,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
assert_eq!(rule.detection.conditions.len(), 1);
if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
assert_eq!(parts.len(), 2);
assert!(
matches!(&parts[1], ConditionExpr::Not(_)),
"Expected negated condition, got: {:?}",
parts[1]
);
} else {
panic!("Expected And condition");
}
}
#[test]
fn test_replace_string_with_detection_item_condition() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let det_conds = vec![DetectionItemCondition::MatchString {
regex: regex::Regex::new("whoami").unwrap(),
negate: false,
}];
let t = Transformation::ReplaceString {
regex: r"whoami".to_string(),
replacement: "REPLACED".to_string(),
skip_special: false,
};
t.apply(&mut rule, &mut state, &det_conds, &[], false)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "REPLACED");
}
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "\\cmd.exe");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_set_value_with_is_null_condition() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![
DetectionItem {
field: FieldSpec::new(Some("FieldA".to_string()), vec![]),
values: vec![SigmaValue::Null],
},
DetectionItem {
field: FieldSpec::new(Some("FieldB".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("value"))],
},
]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let det_conds = vec![DetectionItemCondition::IsNull { negate: false }];
let t = Transformation::SetValue {
value: SigmaValue::String(SigmaString::new("DEFAULT")),
};
t.apply(&mut rule, &mut state, &det_conds, &[], false)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "DEFAULT");
} else {
panic!("Expected String after set_value on null");
}
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "value");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_drop_detection_item_with_match_string_condition() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let det_conds = vec![DetectionItemCondition::MatchString {
regex: regex::Regex::new("whoami").unwrap(),
negate: false,
}];
let t = Transformation::DropDetectionItem;
t.apply(&mut rule, &mut state, &det_conds, &[], false)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items.len(), 1);
assert_eq!(items[0].field.name, Some("ParentImage".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_mapping_with_cond_not() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let field_conds = vec![FieldNameCondition::IncludeFields {
matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
}];
let mut mapping = HashMap::new();
mapping.insert("CommandLine".to_string(), vec!["cmd".to_string()]);
mapping.insert("ParentImage".to_string(), vec!["parent".to_string()]);
let t = Transformation::FieldNameMapping { mapping };
t.apply(&mut rule, &mut state, &[], &field_conds, true)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
assert_eq!(items[1].field.name, Some("parent".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_mapping_empty() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::FieldNameMapping {
mapping: HashMap::new(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_field_name_prefix_mapping_empty() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::FieldNamePrefixMapping {
mapping: HashMap::new(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_map_string_empty_mapping() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::MapString {
mapping: HashMap::new(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det
&& let SigmaValue::String(s) = &items[0].values[0]
{
assert_eq!(s.original, "whoami");
}
}
#[test]
fn test_hashes_fields_empty_algos() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new(
"SHA256=abc123,IMPHASH=def456",
))],
}]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let t = Transformation::HashesFields {
valid_hash_algos: vec![], field_prefix: "File".to_string(),
drop_algo_prefix: false,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items.len(), 2);
assert_eq!(items[0].field.name, Some("FileSHA256".to_string()));
assert_eq!(items[1].field.name, Some("FileIMPHASH".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_hashes_fields_drop_algo_prefix() {
let mut named = HashMap::new();
named.insert(
"selection".to_string(),
Detection::AllOf(vec![DetectionItem {
field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
values: vec![SigmaValue::String(SigmaString::new("MD5=abc123"))],
}]),
);
let mut rule = make_test_rule();
rule.detection.named = named;
let mut state = PipelineState::default();
let t = Transformation::HashesFields {
valid_hash_algos: vec!["MD5".to_string()],
field_prefix: "Hash".to_string(),
drop_algo_prefix: true,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items.len(), 1);
assert_eq!(items[0].field.name, Some("Hash".to_string()));
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_replace_string_invalid_regex() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::ReplaceString {
regex: r"[invalid".to_string(), replacement: "x".to_string(),
skip_special: false,
};
let result = t.apply(&mut rule, &mut state, &[], &[], false);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("bad regex"),
"error should mention regex: {err}"
);
}
#[test]
fn test_case_transformation_with_negated_match_string() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let det_conds = vec![DetectionItemCondition::MatchString {
regex: regex::Regex::new("whoami").unwrap(),
negate: true,
}];
let t = Transformation::CaseTransformation {
case_type: "upper".to_string(),
};
t.apply(&mut rule, &mut state, &det_conds, &[], false)
.unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "whoami");
}
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "\\CMD.EXE");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_multi_transformation_chaining_pipeline() {
use crate::pipeline::parse_pipeline;
let yaml = r#"
name: Multi-step Pipeline
transformations:
- id: step1_map
type: field_name_mapping
mapping:
CommandLine: process.command_line
ParentImage: process.parent.executable
- id: step2_prefix
type: field_name_prefix
prefix: "winlog."
rule_conditions:
- type: logsource
product: windows
- id: step3_case
type: case_transformation
case_type: upper
field_name_conditions:
- type: include_fields
fields:
- winlog.process.command_line
- id: step4_attr
type: set_custom_attribute
attribute: rsigma.processed
value: "true"
"#;
let pipeline = parse_pipeline(yaml).unwrap();
let mut rule = make_test_rule(); let mut state = PipelineState::new(pipeline.vars.clone());
pipeline.apply(&mut rule, &mut state).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(
items[0].field.name,
Some("winlog.process.command_line".to_string())
);
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "WHOAMI");
}
assert_eq!(
items[1].field.name,
Some("winlog.process.parent.executable".to_string())
);
if let SigmaValue::String(s) = &items[1].values[0] {
assert_eq!(s.original, "\\cmd.exe");
}
} else {
panic!("Expected AllOf");
}
assert_eq!(
rule.custom_attributes
.get("rsigma.processed")
.and_then(|v| v.as_str()),
Some("true")
);
assert!(state.was_applied("step1_map"));
assert!(state.was_applied("step2_prefix"));
assert!(state.was_applied("step3_case"));
assert!(state.was_applied("step4_attr"));
}
#[test]
fn test_map_string_one_to_many() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert(
"whoami".to_string(),
vec!["who".to_string(), "am_i".to_string(), "test".to_string()],
);
let t = Transformation::MapString { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].values.len(), 3);
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "who");
}
if let SigmaValue::String(s) = &items[0].values[1] {
assert_eq!(s.original, "am_i");
}
if let SigmaValue::String(s) = &items[0].values[2] {
assert_eq!(s.original, "test");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_map_string_one_to_many_mixed() {
let yaml = r#"
title: Test Rule
logsource:
product: windows
detection:
selection:
CommandLine:
- whoami
- ipconfig
condition: selection
level: medium
"#;
let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
let mut rule = collection.rules[0].clone();
let mut state = PipelineState::default();
let mut mapping = HashMap::new();
mapping.insert(
"whoami".to_string(),
vec!["who".to_string(), "am_i".to_string()],
);
let t = Transformation::MapString { mapping };
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
assert_eq!(items[0].values.len(), 3);
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "who");
}
if let SigmaValue::String(s) = &items[0].values[1] {
assert_eq!(s.original, "am_i");
}
if let SigmaValue::String(s) = &items[0].values[2] {
assert_eq!(s.original, "ipconfig");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_replace_string_skip_special_preserves_wildcards() {
let yaml = r#"
title: Test Rule
logsource:
product: windows
detection:
selection:
CommandLine: '*whoami*'
condition: selection
level: medium
"#;
let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
let mut rule = collection.rules[0].clone();
let mut state = PipelineState::default();
let t = Transformation::ReplaceString {
regex: r"whoami".to_string(),
replacement: "REPLACED".to_string(),
skip_special: true,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
let s = match &items[0].values[0] {
SigmaValue::String(s) => s,
_ => panic!("Expected String"),
};
assert!(s.contains_wildcards(), "Wildcards should be preserved");
assert!(
s.original.contains("REPLACED"),
"Plain part should be replaced, got: {}",
s.original
);
assert!(
!s.original.contains("whoami"),
"Original text should be gone"
);
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_replace_string_skip_special_false_replaces_whole() {
let yaml = r#"
title: Test Rule
logsource:
product: windows
detection:
selection:
CommandLine: '*whoami*'
condition: selection
level: medium
"#;
let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
let mut rule = collection.rules[0].clone();
let mut state = PipelineState::default();
let t = Transformation::ReplaceString {
regex: r"\*".to_string(),
replacement: "STAR".to_string(),
skip_special: false,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
let s = match &items[0].values[0] {
SigmaValue::String(s) => s,
_ => panic!("Expected String"),
};
assert!(
s.original.contains("STAR"),
"Wildcards in original should be replaced as text, got: {}",
s.original
);
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_replace_string_skip_special_plain_string() {
let mut rule = make_test_rule();
let mut state = PipelineState::default();
let t = Transformation::ReplaceString {
regex: r"whoami".to_string(),
replacement: "REPLACED".to_string(),
skip_special: true,
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det
&& let SigmaValue::String(s) = &items[0].values[0]
{
assert_eq!(s.original, "REPLACED");
}
}
#[test]
fn test_case_transformation_snake_case() {
let yaml = r#"
title: Test Rule
logsource:
product: windows
detection:
selection:
CommandLine: CommandAndControl
condition: selection
level: medium
"#;
let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
let mut rule = collection.rules[0].clone();
let mut state = PipelineState::default();
let t = Transformation::CaseTransformation {
case_type: "snake_case".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det {
if let SigmaValue::String(s) = &items[0].values[0] {
assert_eq!(s.original, "command_and_control");
} else {
panic!("Expected String");
}
} else {
panic!("Expected AllOf");
}
}
#[test]
fn test_case_transformation_snake_case_already_lowercase() {
let mut rule = make_test_rule(); let mut state = PipelineState::default();
let t = Transformation::CaseTransformation {
case_type: "snake_case".to_string(),
};
t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
let det = &rule.detection.named["selection"];
if let Detection::AllOf(items) = det
&& let SigmaValue::String(s) = &items[0].values[0]
{
assert_eq!(s.original, "whoami"); }
}