1use std::collections::HashMap;
8
9use regex::Regex;
10
11use rsigma_parser::{
12 ConditionExpr, Detection, DetectionItem, FieldSpec, SigmaRule, SigmaString, SigmaValue,
13 SpecialChar, StringPart,
14};
15
16use super::conditions::{DetectionItemCondition, FieldNameCondition};
17use super::state::PipelineState;
18use crate::error::{EvalError, Result};
19
20#[derive(Debug, Clone)]
26pub enum Transformation {
27 FieldNameMapping { mapping: HashMap<String, String> },
29
30 FieldNamePrefixMapping { mapping: HashMap<String, String> },
32
33 FieldNamePrefix { prefix: String },
35
36 FieldNameSuffix { suffix: String },
38
39 DropDetectionItem,
41
42 AddCondition {
44 conditions: HashMap<String, SigmaValue>,
45 negated: bool,
47 },
48
49 ChangeLogsource {
51 category: Option<String>,
52 product: Option<String>,
53 service: Option<String>,
54 },
55
56 ReplaceString {
62 regex: String,
63 replacement: String,
64 skip_special: bool,
65 },
66
67 ValuePlaceholders,
69
70 WildcardPlaceholders,
72
73 QueryExpressionPlaceholders { expression: String },
75
76 SetState { key: String, value: String },
78
79 RuleFailure { message: String },
81
82 DetectionItemFailure { message: String },
84
85 FieldNameTransform {
88 transform_func: String,
90 mapping: HashMap<String, String>,
92 },
93
94 HashesFields {
98 valid_hash_algos: Vec<String>,
100 field_prefix: String,
102 drop_algo_prefix: bool,
104 },
105
106 MapString {
112 mapping: HashMap<String, Vec<String>>,
113 },
114
115 SetValue { value: SigmaValue },
117
118 ConvertType { target_type: String },
121
122 Regex,
124
125 AddField { field: String },
127
128 RemoveField { field: String },
130
131 SetField { fields: Vec<String> },
133
134 SetCustomAttribute { attribute: String, value: String },
141
142 CaseTransformation { case_type: String },
145
146 Nest {
149 items: Vec<super::TransformationItem>,
150 },
151}
152
153impl Transformation {
158 pub fn apply(
162 &self,
163 rule: &mut SigmaRule,
164 state: &mut PipelineState,
165 detection_item_conditions: &[DetectionItemCondition],
166 field_name_conditions: &[FieldNameCondition],
167 field_name_cond_not: bool,
168 ) -> Result<bool> {
169 match self {
170 Transformation::FieldNameMapping { mapping } => {
171 apply_field_name_transform(
172 rule,
173 state,
174 field_name_conditions,
175 field_name_cond_not,
176 |name| mapping.get(name).cloned(),
177 );
178 Ok(true)
179 }
180
181 Transformation::FieldNamePrefixMapping { mapping } => {
182 apply_field_name_transform(
183 rule,
184 state,
185 field_name_conditions,
186 field_name_cond_not,
187 |name| {
188 for (prefix, replacement) in mapping {
189 if name.starts_with(prefix.as_str()) {
190 return Some(format!("{}{}", replacement, &name[prefix.len()..]));
191 }
192 }
193 None
194 },
195 );
196 Ok(true)
197 }
198
199 Transformation::FieldNamePrefix { prefix } => {
200 apply_field_name_transform(
201 rule,
202 state,
203 field_name_conditions,
204 field_name_cond_not,
205 |name| Some(format!("{prefix}{name}")),
206 );
207 Ok(true)
208 }
209
210 Transformation::FieldNameSuffix { suffix } => {
211 apply_field_name_transform(
212 rule,
213 state,
214 field_name_conditions,
215 field_name_cond_not,
216 |name| Some(format!("{name}{suffix}")),
217 );
218 Ok(true)
219 }
220
221 Transformation::DropDetectionItem => {
222 drop_detection_items(
223 rule,
224 state,
225 detection_item_conditions,
226 field_name_conditions,
227 field_name_cond_not,
228 );
229 Ok(true)
230 }
231
232 Transformation::AddCondition {
233 conditions,
234 negated,
235 } => {
236 add_conditions(rule, conditions, *negated);
237 Ok(true)
238 }
239
240 Transformation::ChangeLogsource {
241 category,
242 product,
243 service,
244 } => {
245 if let Some(cat) = category {
246 rule.logsource.category = Some(cat.clone());
247 }
248 if let Some(prod) = product {
249 rule.logsource.product = Some(prod.clone());
250 }
251 if let Some(svc) = service {
252 rule.logsource.service = Some(svc.clone());
253 }
254 Ok(true)
255 }
256
257 Transformation::ReplaceString {
258 regex,
259 replacement,
260 skip_special,
261 } => {
262 let re = Regex::new(regex)
263 .map_err(|e| EvalError::InvalidModifiers(format!("bad regex: {e}")))?;
264 replace_strings_in_rule(
265 rule,
266 state,
267 detection_item_conditions,
268 field_name_conditions,
269 field_name_cond_not,
270 &re,
271 replacement,
272 *skip_special,
273 );
274 Ok(true)
275 }
276
277 Transformation::ValuePlaceholders => {
278 expand_placeholders_in_rule(rule, state, false);
279 Ok(true)
280 }
281
282 Transformation::WildcardPlaceholders => {
283 expand_placeholders_in_rule(rule, state, true);
284 Ok(true)
285 }
286
287 Transformation::QueryExpressionPlaceholders { .. } => {
288 Ok(false)
290 }
291
292 Transformation::SetState { key, value } => {
293 state.set_state(key.clone(), serde_json::Value::String(value.clone()));
294 Ok(true)
295 }
296
297 Transformation::RuleFailure { message } => Err(EvalError::InvalidModifiers(format!(
298 "Pipeline rule failure: {message} (rule: {})",
299 rule.title
300 ))),
301
302 Transformation::DetectionItemFailure { message } => {
303 let has_match = rule_has_matching_item(rule, state, detection_item_conditions);
305 if has_match {
306 Err(EvalError::InvalidModifiers(format!(
307 "Pipeline detection item failure: {message} (rule: {})",
308 rule.title
309 )))
310 } else {
311 Ok(false)
312 }
313 }
314
315 Transformation::FieldNameTransform {
316 transform_func,
317 mapping,
318 } => {
319 let func = transform_func.clone();
320 let map = mapping.clone();
321 apply_field_name_transform(
322 rule,
323 state,
324 field_name_conditions,
325 field_name_cond_not,
326 |name| {
327 if let Some(mapped) = map.get(name) {
328 return Some(mapped.clone());
329 }
330 Some(apply_named_string_fn(&func, name))
331 },
332 );
333 Ok(true)
334 }
335
336 Transformation::HashesFields {
337 valid_hash_algos,
338 field_prefix,
339 drop_algo_prefix,
340 } => {
341 decompose_hashes_field(rule, valid_hash_algos, field_prefix, *drop_algo_prefix);
342 Ok(true)
343 }
344
345 Transformation::MapString { mapping } => {
346 map_string_values(
347 rule,
348 state,
349 detection_item_conditions,
350 field_name_conditions,
351 field_name_cond_not,
352 mapping,
353 );
354 Ok(true)
355 }
356
357 Transformation::SetValue { value } => {
358 set_detection_item_values(
359 rule,
360 state,
361 detection_item_conditions,
362 field_name_conditions,
363 field_name_cond_not,
364 value,
365 );
366 Ok(true)
367 }
368
369 Transformation::ConvertType { target_type } => {
370 convert_detection_item_types(
371 rule,
372 state,
373 detection_item_conditions,
374 field_name_conditions,
375 field_name_cond_not,
376 target_type,
377 );
378 Ok(true)
379 }
380
381 Transformation::Regex => {
382 Ok(false)
386 }
387
388 Transformation::AddField { field } => {
389 if !rule.fields.contains(field) {
390 rule.fields.push(field.clone());
391 }
392 Ok(true)
393 }
394
395 Transformation::RemoveField { field } => {
396 rule.fields.retain(|f| f != field);
397 Ok(true)
398 }
399
400 Transformation::SetField { fields } => {
401 rule.fields = fields.clone();
402 Ok(true)
403 }
404
405 Transformation::SetCustomAttribute { attribute, value } => {
406 rule.custom_attributes
407 .insert(attribute.clone(), serde_yaml::Value::String(value.clone()));
408 Ok(true)
409 }
410
411 Transformation::CaseTransformation { case_type } => {
412 apply_case_transformation(
413 rule,
414 state,
415 detection_item_conditions,
416 field_name_conditions,
417 field_name_cond_not,
418 case_type,
419 );
420 Ok(true)
421 }
422
423 Transformation::Nest { items } => {
424 for item in items {
425 let mut merged_det_conds: Vec<DetectionItemCondition> =
427 detection_item_conditions.to_vec();
428 merged_det_conds.extend(item.detection_item_conditions.clone());
429
430 let mut merged_field_conds: Vec<FieldNameCondition> =
431 field_name_conditions.to_vec();
432 merged_field_conds.extend(item.field_name_conditions.clone());
433
434 let rule_ok = if item.rule_conditions.is_empty() {
436 true
437 } else {
438 super::conditions::all_rule_conditions_match(
439 &item.rule_conditions,
440 rule,
441 state,
442 )
443 };
444
445 if rule_ok {
446 item.transformation.apply(
447 rule,
448 state,
449 &merged_det_conds,
450 &merged_field_conds,
451 item.field_name_cond_not || field_name_cond_not,
452 )?;
453 if let Some(ref id) = item.id {
454 state.mark_applied(id);
455 }
456 }
457 }
458 Ok(true)
459 }
460 }
461 }
462}
463
464fn apply_field_name_transform<F>(
469 rule: &mut SigmaRule,
470 state: &PipelineState,
471 field_name_conditions: &[FieldNameCondition],
472 field_name_cond_not: bool,
473 transform_fn: F,
474) where
475 F: Fn(&str) -> Option<String>,
476{
477 for detection in rule.detection.named.values_mut() {
478 transform_detection_fields(
479 detection,
480 state,
481 field_name_conditions,
482 field_name_cond_not,
483 &transform_fn,
484 );
485 }
486}
487
488fn transform_detection_fields<F>(
489 detection: &mut Detection,
490 state: &PipelineState,
491 field_name_conditions: &[FieldNameCondition],
492 field_name_cond_not: bool,
493 transform_fn: &F,
494) where
495 F: Fn(&str) -> Option<String>,
496{
497 match detection {
498 Detection::AllOf(items) => {
499 for item in items.iter_mut() {
500 if let Some(ref name) = item.field.name
501 && field_conditions_match(
502 name,
503 state,
504 field_name_conditions,
505 field_name_cond_not,
506 )
507 && let Some(new_name) = transform_fn(name)
508 {
509 item.field.name = Some(new_name);
510 }
511 }
512 }
513 Detection::AnyOf(subs) => {
514 for sub in subs.iter_mut() {
515 transform_detection_fields(
516 sub,
517 state,
518 field_name_conditions,
519 field_name_cond_not,
520 transform_fn,
521 );
522 }
523 }
524 Detection::Keywords(_) => {}
525 }
526}
527
528fn field_conditions_match(
529 field_name: &str,
530 state: &PipelineState,
531 conditions: &[FieldNameCondition],
532 negate: bool,
533) -> bool {
534 if conditions.is_empty() {
535 return true;
536 }
537 let all_match = conditions
538 .iter()
539 .all(|c| c.matches_field_name(field_name, state));
540 if negate { !all_match } else { all_match }
541}
542
543fn drop_detection_items(
548 rule: &mut SigmaRule,
549 state: &PipelineState,
550 detection_conditions: &[DetectionItemCondition],
551 field_name_conditions: &[FieldNameCondition],
552 field_name_cond_not: bool,
553) {
554 for detection in rule.detection.named.values_mut() {
555 drop_from_detection(
556 detection,
557 state,
558 detection_conditions,
559 field_name_conditions,
560 field_name_cond_not,
561 );
562 }
563}
564
565fn drop_from_detection(
566 detection: &mut Detection,
567 state: &PipelineState,
568 detection_conditions: &[DetectionItemCondition],
569 field_name_conditions: &[FieldNameCondition],
570 field_name_cond_not: bool,
571) {
572 match detection {
573 Detection::AllOf(items) => {
574 items.retain(|item| {
575 !should_drop_item(
576 item,
577 state,
578 detection_conditions,
579 field_name_conditions,
580 field_name_cond_not,
581 )
582 });
583 }
584 Detection::AnyOf(subs) => {
585 for sub in subs.iter_mut() {
586 drop_from_detection(
587 sub,
588 state,
589 detection_conditions,
590 field_name_conditions,
591 field_name_cond_not,
592 );
593 }
594 }
595 Detection::Keywords(_) => {}
596 }
597}
598
599fn should_drop_item(
600 item: &DetectionItem,
601 state: &PipelineState,
602 detection_conditions: &[DetectionItemCondition],
603 field_name_conditions: &[FieldNameCondition],
604 field_name_cond_not: bool,
605) -> bool {
606 let det_match = detection_conditions.is_empty()
608 || detection_conditions
609 .iter()
610 .all(|c| c.matches_item(item, state));
611
612 let field_match = if let Some(ref name) = item.field.name {
614 field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
615 } else {
616 field_name_conditions.is_empty()
617 };
618
619 det_match && field_match
620}
621
622fn add_conditions(rule: &mut SigmaRule, conditions: &HashMap<String, SigmaValue>, negated: bool) {
627 let items: Vec<DetectionItem> = conditions
629 .iter()
630 .map(|(field, value)| DetectionItem {
631 field: FieldSpec::new(Some(field.clone()), Vec::new()),
632 values: vec![value.clone()],
633 })
634 .collect();
635
636 let det_name = format!("__pipeline_cond_{}", rule.detection.named.len());
637 rule.detection
638 .named
639 .insert(det_name.clone(), Detection::AllOf(items));
640
641 let cond_ref = ConditionExpr::Identifier(det_name);
643 let cond_expr = if negated {
644 ConditionExpr::Not(Box::new(cond_ref))
645 } else {
646 cond_ref
647 };
648
649 rule.detection.conditions = rule
650 .detection
651 .conditions
652 .iter()
653 .map(|existing| ConditionExpr::And(vec![existing.clone(), cond_expr.clone()]))
654 .collect();
655}
656
657#[allow(clippy::too_many_arguments)]
662fn replace_strings_in_rule(
663 rule: &mut SigmaRule,
664 state: &PipelineState,
665 detection_conditions: &[DetectionItemCondition],
666 field_name_conditions: &[FieldNameCondition],
667 field_name_cond_not: bool,
668 re: &Regex,
669 replacement: &str,
670 skip_special: bool,
671) {
672 for detection in rule.detection.named.values_mut() {
673 replace_strings_in_detection(
674 detection,
675 state,
676 detection_conditions,
677 field_name_conditions,
678 field_name_cond_not,
679 re,
680 replacement,
681 skip_special,
682 );
683 }
684}
685
686#[allow(clippy::too_many_arguments)]
687fn replace_strings_in_detection(
688 detection: &mut Detection,
689 state: &PipelineState,
690 detection_conditions: &[DetectionItemCondition],
691 field_name_conditions: &[FieldNameCondition],
692 field_name_cond_not: bool,
693 re: &Regex,
694 replacement: &str,
695 skip_special: bool,
696) {
697 match detection {
698 Detection::AllOf(items) => {
699 for item in items.iter_mut() {
700 let det_match = detection_conditions.is_empty()
701 || detection_conditions
702 .iter()
703 .all(|c| c.matches_item(item, state));
704 let field_match = if let Some(ref name) = item.field.name {
705 field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
706 } else {
707 field_name_conditions.is_empty()
708 };
709
710 if det_match && field_match {
711 replace_strings_in_values(&mut item.values, re, replacement, skip_special);
712 }
713 }
714 }
715 Detection::AnyOf(subs) => {
716 for sub in subs.iter_mut() {
717 replace_strings_in_detection(
718 sub,
719 state,
720 detection_conditions,
721 field_name_conditions,
722 field_name_cond_not,
723 re,
724 replacement,
725 skip_special,
726 );
727 }
728 }
729 Detection::Keywords(values) => {
730 replace_strings_in_values(values, re, replacement, skip_special);
731 }
732 }
733}
734
735fn replace_strings_in_values(
736 values: &mut [SigmaValue],
737 re: &Regex,
738 replacement: &str,
739 skip_special: bool,
740) {
741 for value in values.iter_mut() {
742 if let SigmaValue::String(s) = value {
743 if skip_special && s.contains_wildcards() {
744 let new_parts: Vec<StringPart> = s
746 .parts
747 .iter()
748 .map(|part| match part {
749 StringPart::Plain(text) => {
750 let replaced = re.replace_all(text, replacement);
751 StringPart::Plain(replaced.into_owned())
752 }
753 special => special.clone(),
754 })
755 .collect();
756 if new_parts != s.parts {
757 let new_original = parts_to_original(&new_parts);
759 s.parts = new_parts;
760 s.original = new_original;
761 }
762 } else {
763 let replaced = re.replace_all(&s.original, replacement);
764 if replaced != s.original {
765 *s = SigmaString::new(&replaced);
766 }
767 }
768 }
769 }
770}
771
772fn parts_to_original(parts: &[StringPart]) -> String {
774 let mut out = String::new();
775 for part in parts {
776 match part {
777 StringPart::Plain(text) => {
778 for c in text.chars() {
780 if c == '*' || c == '?' || c == '\\' {
781 out.push('\\');
782 }
783 out.push(c);
784 }
785 }
786 StringPart::Special(SpecialChar::WildcardMulti) => out.push('*'),
787 StringPart::Special(SpecialChar::WildcardSingle) => out.push('?'),
788 }
789 }
790 out
791}
792
793fn expand_placeholders_in_rule(rule: &mut SigmaRule, state: &PipelineState, wildcard: bool) {
798 for detection in rule.detection.named.values_mut() {
799 expand_placeholders_in_detection(detection, state, wildcard);
800 }
801}
802
803fn expand_placeholders_in_detection(
804 detection: &mut Detection,
805 state: &PipelineState,
806 wildcard: bool,
807) {
808 match detection {
809 Detection::AllOf(items) => {
810 for item in items.iter_mut() {
811 expand_placeholders_in_values(&mut item.values, state, wildcard);
812 }
813 }
814 Detection::AnyOf(subs) => {
815 for sub in subs.iter_mut() {
816 expand_placeholders_in_detection(sub, state, wildcard);
817 }
818 }
819 Detection::Keywords(values) => {
820 expand_placeholders_in_values(values, state, wildcard);
821 }
822 }
823}
824
825fn expand_placeholders_in_values(
826 values: &mut Vec<SigmaValue>,
827 state: &PipelineState,
828 wildcard: bool,
829) {
830 let mut expanded_values = Vec::new();
831 for value in values.drain(..) {
832 if let SigmaValue::String(ref s) = value {
833 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
834 if plain.contains('%') {
835 let result = expand_placeholder_string(&plain, state, wildcard);
837 expanded_values.extend(result);
838 continue;
839 }
840 }
841 expanded_values.push(value);
842 }
843 *values = expanded_values;
844}
845
846fn expand_placeholder_string(s: &str, state: &PipelineState, wildcard: bool) -> Vec<SigmaValue> {
847 let mut result = s.to_string();
849 let mut has_unresolved = false;
850
851 while let Some(start) = result.find('%') {
853 let rest = &result[start + 1..];
854 let Some(end) = rest.find('%') else {
855 break;
856 };
857 let placeholder = &rest[..end];
858
859 if let Some(values) = state.vars.get(placeholder) {
860 if values.len() == 1 {
861 result = format!("{}{}{}", &result[..start], values[0], &rest[end + 1..]);
862 } else if values.is_empty() {
863 if wildcard {
864 result = format!("{}*{}", &result[..start], &rest[end + 1..]);
865 } else {
866 has_unresolved = true;
867 break;
868 }
869 } else {
870 return values
872 .iter()
873 .map(|v| {
874 let expanded = format!("{}{}{}", &result[..start], v, &rest[end + 1..]);
875 SigmaValue::String(SigmaString::new(&expanded))
876 })
877 .collect();
878 }
879 } else if wildcard {
880 result = format!("{}*{}", &result[..start], &rest[end + 1..]);
881 } else {
882 has_unresolved = true;
883 break;
884 }
885 }
886
887 if has_unresolved && wildcard {
888 vec![SigmaValue::String(SigmaString::new(&result))]
891 } else {
892 vec![SigmaValue::String(SigmaString::new(&result))]
893 }
894}
895
896fn apply_named_string_fn(func: &str, s: &str) -> String {
901 match func {
902 "lower" | "lowercase" => s.to_lowercase(),
903 "upper" | "uppercase" => s.to_uppercase(),
904 "title" => {
905 s.split(|c: char| !c.is_alphanumeric())
907 .filter(|w| !w.is_empty())
908 .map(|w| {
909 let mut c = w.chars();
910 match c.next() {
911 None => String::new(),
912 Some(f) => {
913 f.to_uppercase().collect::<String>() + &c.as_str().to_lowercase()
914 }
915 }
916 })
917 .collect::<Vec<_>>()
918 .join("_")
919 }
920 "snake_case" => {
921 let mut out = String::new();
923 for (i, ch) in s.chars().enumerate() {
924 if ch.is_uppercase() && i > 0 {
925 out.push('_');
926 }
927 out.push(ch.to_lowercase().next().unwrap_or(ch));
928 }
929 out
930 }
931 _ => s.to_string(), }
933}
934
935fn decompose_hashes_field(
940 rule: &mut SigmaRule,
941 valid_algos: &[String],
942 field_prefix: &str,
943 drop_algo_prefix: bool,
944) {
945 for detection in rule.detection.named.values_mut() {
946 decompose_hashes_in_detection(detection, valid_algos, field_prefix, drop_algo_prefix);
947 }
948}
949
950fn decompose_hashes_in_detection(
951 detection: &mut Detection,
952 valid_algos: &[String],
953 field_prefix: &str,
954 drop_algo_prefix: bool,
955) {
956 match detection {
957 Detection::AllOf(items) => {
958 let mut new_items: Vec<DetectionItem> = Vec::new();
959 let mut i = 0;
960 while i < items.len() {
961 let item = &items[i];
962 let is_hashes = item
963 .field
964 .name
965 .as_deref()
966 .map(|n| n.eq_ignore_ascii_case("hashes"))
967 .unwrap_or(false);
968
969 if is_hashes {
970 for val in &item.values {
972 if let SigmaValue::String(s) = val {
973 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
974 for pair in plain.split(',') {
975 let pair = pair.trim();
976 if let Some((algo, hash)) = pair.split_once('=') {
977 let algo_upper = algo.trim().to_uppercase();
978 if valid_algos.is_empty()
979 || valid_algos
980 .iter()
981 .any(|a| a.eq_ignore_ascii_case(&algo_upper))
982 {
983 let field_name = if drop_algo_prefix {
984 field_prefix.to_string()
985 } else {
986 format!("{field_prefix}{}", algo.trim())
987 };
988 new_items.push(DetectionItem {
989 field: FieldSpec::new(
990 Some(field_name),
991 item.field.modifiers.clone(),
992 ),
993 values: vec![SigmaValue::String(SigmaString::new(
994 hash.trim(),
995 ))],
996 });
997 }
998 }
999 }
1000 }
1001 }
1002 } else {
1003 new_items.push(items[i].clone());
1004 }
1005 i += 1;
1006 }
1007 *items = new_items;
1008 }
1009 Detection::AnyOf(subs) => {
1010 for sub in subs.iter_mut() {
1011 decompose_hashes_in_detection(sub, valid_algos, field_prefix, drop_algo_prefix);
1012 }
1013 }
1014 Detection::Keywords(_) => {}
1015 }
1016}
1017
1018fn map_string_values(
1023 rule: &mut SigmaRule,
1024 state: &PipelineState,
1025 detection_conditions: &[DetectionItemCondition],
1026 field_name_conditions: &[FieldNameCondition],
1027 field_name_cond_not: bool,
1028 mapping: &HashMap<String, Vec<String>>,
1029) {
1030 for detection in rule.detection.named.values_mut() {
1031 map_strings_in_detection(
1032 detection,
1033 state,
1034 detection_conditions,
1035 field_name_conditions,
1036 field_name_cond_not,
1037 mapping,
1038 );
1039 }
1040}
1041
1042fn map_strings_in_detection(
1043 detection: &mut Detection,
1044 state: &PipelineState,
1045 detection_conditions: &[DetectionItemCondition],
1046 field_name_conditions: &[FieldNameCondition],
1047 field_name_cond_not: bool,
1048 mapping: &HashMap<String, Vec<String>>,
1049) {
1050 match detection {
1051 Detection::AllOf(items) => {
1052 for item in items.iter_mut() {
1053 if item_conditions_match(
1054 item,
1055 state,
1056 detection_conditions,
1057 field_name_conditions,
1058 field_name_cond_not,
1059 ) {
1060 map_string_expand_values(&mut item.values, mapping);
1061 }
1062 }
1063 }
1064 Detection::AnyOf(subs) => {
1065 for sub in subs.iter_mut() {
1066 map_strings_in_detection(
1067 sub,
1068 state,
1069 detection_conditions,
1070 field_name_conditions,
1071 field_name_cond_not,
1072 mapping,
1073 );
1074 }
1075 }
1076 Detection::Keywords(values) => {
1077 map_string_expand_values(values, mapping);
1078 }
1079 }
1080}
1081
1082fn map_string_expand_values(values: &mut Vec<SigmaValue>, mapping: &HashMap<String, Vec<String>>) {
1089 let mut extra: Vec<(usize, Vec<SigmaValue>)> = Vec::new();
1090
1091 for (i, val) in values.iter_mut().enumerate() {
1092 if let SigmaValue::String(s) = val {
1093 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1094 if let Some(replacements) = mapping.get(&plain) {
1095 if let Some(first) = replacements.first() {
1096 *s = SigmaString::new(first);
1097 }
1098 if replacements.len() > 1 {
1100 let extras: Vec<SigmaValue> = replacements[1..]
1101 .iter()
1102 .map(|r| SigmaValue::String(SigmaString::new(r)))
1103 .collect();
1104 extra.push((i, extras));
1105 }
1106 }
1107 }
1108 }
1109
1110 for (idx, extras) in extra.into_iter().rev() {
1112 for (j, v) in extras.into_iter().enumerate() {
1113 values.insert(idx + 1 + j, v);
1114 }
1115 }
1116}
1117
1118fn set_detection_item_values(
1123 rule: &mut SigmaRule,
1124 state: &PipelineState,
1125 detection_conditions: &[DetectionItemCondition],
1126 field_name_conditions: &[FieldNameCondition],
1127 field_name_cond_not: bool,
1128 value: &SigmaValue,
1129) {
1130 for detection in rule.detection.named.values_mut() {
1131 set_values_in_detection(
1132 detection,
1133 state,
1134 detection_conditions,
1135 field_name_conditions,
1136 field_name_cond_not,
1137 value,
1138 );
1139 }
1140}
1141
1142fn set_values_in_detection(
1143 detection: &mut Detection,
1144 state: &PipelineState,
1145 detection_conditions: &[DetectionItemCondition],
1146 field_name_conditions: &[FieldNameCondition],
1147 field_name_cond_not: bool,
1148 value: &SigmaValue,
1149) {
1150 match detection {
1151 Detection::AllOf(items) => {
1152 for item in items.iter_mut() {
1153 if item_conditions_match(
1154 item,
1155 state,
1156 detection_conditions,
1157 field_name_conditions,
1158 field_name_cond_not,
1159 ) {
1160 item.values = vec![value.clone()];
1161 }
1162 }
1163 }
1164 Detection::AnyOf(subs) => {
1165 for sub in subs.iter_mut() {
1166 set_values_in_detection(
1167 sub,
1168 state,
1169 detection_conditions,
1170 field_name_conditions,
1171 field_name_cond_not,
1172 value,
1173 );
1174 }
1175 }
1176 Detection::Keywords(_) => {}
1177 }
1178}
1179
1180fn convert_detection_item_types(
1185 rule: &mut SigmaRule,
1186 state: &PipelineState,
1187 detection_conditions: &[DetectionItemCondition],
1188 field_name_conditions: &[FieldNameCondition],
1189 field_name_cond_not: bool,
1190 target_type: &str,
1191) {
1192 for detection in rule.detection.named.values_mut() {
1193 convert_types_in_detection(
1194 detection,
1195 state,
1196 detection_conditions,
1197 field_name_conditions,
1198 field_name_cond_not,
1199 target_type,
1200 );
1201 }
1202}
1203
1204fn convert_types_in_detection(
1205 detection: &mut Detection,
1206 state: &PipelineState,
1207 detection_conditions: &[DetectionItemCondition],
1208 field_name_conditions: &[FieldNameCondition],
1209 field_name_cond_not: bool,
1210 target_type: &str,
1211) {
1212 match detection {
1213 Detection::AllOf(items) => {
1214 for item in items.iter_mut() {
1215 if item_conditions_match(
1216 item,
1217 state,
1218 detection_conditions,
1219 field_name_conditions,
1220 field_name_cond_not,
1221 ) {
1222 for val in item.values.iter_mut() {
1223 *val = convert_value(val, target_type);
1224 }
1225 }
1226 }
1227 }
1228 Detection::AnyOf(subs) => {
1229 for sub in subs.iter_mut() {
1230 convert_types_in_detection(
1231 sub,
1232 state,
1233 detection_conditions,
1234 field_name_conditions,
1235 field_name_cond_not,
1236 target_type,
1237 );
1238 }
1239 }
1240 Detection::Keywords(_) => {}
1241 }
1242}
1243
1244fn convert_value(val: &SigmaValue, target: &str) -> SigmaValue {
1245 match target {
1246 "str" | "string" => match val {
1247 SigmaValue::String(_) => val.clone(),
1248 SigmaValue::Integer(n) => SigmaValue::String(SigmaString::new(&n.to_string())),
1249 SigmaValue::Float(f) => SigmaValue::String(SigmaString::new(&f.to_string())),
1250 SigmaValue::Bool(b) => SigmaValue::String(SigmaString::new(&b.to_string())),
1251 SigmaValue::Null => SigmaValue::String(SigmaString::new("null")),
1252 },
1253 "int" | "integer" => match val {
1254 SigmaValue::String(s) => {
1255 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1256 plain
1257 .parse::<i64>()
1258 .map(SigmaValue::Integer)
1259 .unwrap_or_else(|_| val.clone())
1260 }
1261 SigmaValue::Float(f) => SigmaValue::Integer(*f as i64),
1262 SigmaValue::Bool(b) => SigmaValue::Integer(if *b { 1 } else { 0 }),
1263 _ => val.clone(),
1264 },
1265 "float" => match val {
1266 SigmaValue::String(s) => {
1267 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1268 plain
1269 .parse::<f64>()
1270 .map(SigmaValue::Float)
1271 .unwrap_or_else(|_| val.clone())
1272 }
1273 SigmaValue::Integer(n) => SigmaValue::Float(*n as f64),
1274 SigmaValue::Bool(b) => SigmaValue::Float(if *b { 1.0 } else { 0.0 }),
1275 _ => val.clone(),
1276 },
1277 "bool" | "boolean" => match val {
1278 SigmaValue::String(s) => {
1279 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1280 match plain.to_lowercase().as_str() {
1281 "true" | "1" | "yes" => SigmaValue::Bool(true),
1282 "false" | "0" | "no" => SigmaValue::Bool(false),
1283 _ => val.clone(),
1284 }
1285 }
1286 SigmaValue::Integer(n) => SigmaValue::Bool(*n != 0),
1287 SigmaValue::Float(f) => SigmaValue::Bool(*f != 0.0),
1288 _ => val.clone(),
1289 },
1290 _ => val.clone(),
1291 }
1292}
1293
1294fn apply_case_transformation(
1299 rule: &mut SigmaRule,
1300 state: &PipelineState,
1301 detection_conditions: &[DetectionItemCondition],
1302 field_name_conditions: &[FieldNameCondition],
1303 field_name_cond_not: bool,
1304 case_type: &str,
1305) {
1306 for detection in rule.detection.named.values_mut() {
1307 apply_case_in_detection(
1308 detection,
1309 state,
1310 detection_conditions,
1311 field_name_conditions,
1312 field_name_cond_not,
1313 case_type,
1314 );
1315 }
1316}
1317
1318fn apply_case_in_detection(
1319 detection: &mut Detection,
1320 state: &PipelineState,
1321 detection_conditions: &[DetectionItemCondition],
1322 field_name_conditions: &[FieldNameCondition],
1323 field_name_cond_not: bool,
1324 case_type: &str,
1325) {
1326 match detection {
1327 Detection::AllOf(items) => {
1328 for item in items.iter_mut() {
1329 if item_conditions_match(
1330 item,
1331 state,
1332 detection_conditions,
1333 field_name_conditions,
1334 field_name_cond_not,
1335 ) {
1336 for val in item.values.iter_mut() {
1337 apply_case_to_value(val, case_type);
1338 }
1339 }
1340 }
1341 }
1342 Detection::AnyOf(subs) => {
1343 for sub in subs.iter_mut() {
1344 apply_case_in_detection(
1345 sub,
1346 state,
1347 detection_conditions,
1348 field_name_conditions,
1349 field_name_cond_not,
1350 case_type,
1351 );
1352 }
1353 }
1354 Detection::Keywords(values) => {
1355 for val in values.iter_mut() {
1356 apply_case_to_value(val, case_type);
1357 }
1358 }
1359 }
1360}
1361
1362fn apply_case_to_value(val: &mut SigmaValue, case_type: &str) {
1363 if let SigmaValue::String(s) = val {
1364 let transformed = match case_type {
1365 "lower" | "lowercase" => s.original.to_lowercase(),
1366 "upper" | "uppercase" => s.original.to_uppercase(),
1367 "snake_case" => apply_named_string_fn("snake_case", &s.original),
1368 _ => return,
1369 };
1370 if transformed != s.original {
1371 *s = SigmaString::new(&transformed);
1372 }
1373 }
1374}
1375
1376fn item_conditions_match(
1381 item: &DetectionItem,
1382 state: &PipelineState,
1383 detection_conditions: &[DetectionItemCondition],
1384 field_name_conditions: &[FieldNameCondition],
1385 field_name_cond_not: bool,
1386) -> bool {
1387 let det_match = detection_conditions.is_empty()
1388 || detection_conditions
1389 .iter()
1390 .all(|c| c.matches_item(item, state));
1391
1392 let field_match = if let Some(ref name) = item.field.name {
1393 field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
1394 } else {
1395 field_name_conditions.is_empty()
1396 };
1397
1398 det_match && field_match
1399}
1400
1401fn rule_has_matching_item(
1406 rule: &SigmaRule,
1407 state: &PipelineState,
1408 conditions: &[DetectionItemCondition],
1409) -> bool {
1410 for detection in rule.detection.named.values() {
1411 if detection_has_matching_item(detection, state, conditions) {
1412 return true;
1413 }
1414 }
1415 false
1416}
1417
1418fn detection_has_matching_item(
1419 detection: &Detection,
1420 state: &PipelineState,
1421 conditions: &[DetectionItemCondition],
1422) -> bool {
1423 match detection {
1424 Detection::AllOf(items) => items
1425 .iter()
1426 .any(|item| conditions.iter().all(|c| c.matches_item(item, state))),
1427 Detection::AnyOf(subs) => subs
1428 .iter()
1429 .any(|sub| detection_has_matching_item(sub, state, conditions)),
1430 Detection::Keywords(_) => false,
1431 }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436 use super::*;
1437 use rsigma_parser::{Detections, LogSource, Modifier};
1438
1439 fn make_test_rule() -> SigmaRule {
1440 let mut named = HashMap::new();
1441 named.insert(
1442 "selection".to_string(),
1443 Detection::AllOf(vec![
1444 DetectionItem {
1445 field: FieldSpec::new(
1446 Some("CommandLine".to_string()),
1447 vec![Modifier::Contains],
1448 ),
1449 values: vec![SigmaValue::String(SigmaString::new("whoami"))],
1450 },
1451 DetectionItem {
1452 field: FieldSpec::new(
1453 Some("ParentImage".to_string()),
1454 vec![Modifier::EndsWith],
1455 ),
1456 values: vec![SigmaValue::String(SigmaString::new("\\cmd.exe"))],
1457 },
1458 ]),
1459 );
1460
1461 SigmaRule {
1462 title: "Test Rule".to_string(),
1463 logsource: LogSource {
1464 category: Some("process_creation".to_string()),
1465 product: Some("windows".to_string()),
1466 service: None,
1467 definition: None,
1468 custom: HashMap::new(),
1469 },
1470 detection: Detections {
1471 named,
1472 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
1473 condition_strings: vec!["selection".to_string()],
1474 timeframe: None,
1475 },
1476 id: Some("test-001".to_string()),
1477 name: None,
1478 related: vec![],
1479 taxonomy: None,
1480 status: None,
1481 description: None,
1482 license: None,
1483 author: None,
1484 references: vec![],
1485 date: None,
1486 modified: None,
1487 fields: vec![],
1488 falsepositives: vec![],
1489 level: Some(rsigma_parser::Level::Medium),
1490 tags: vec![],
1491 scope: vec![],
1492 custom_attributes: HashMap::new(),
1493 }
1494 }
1495
1496 #[test]
1497 fn test_field_name_mapping() {
1498 let mut rule = make_test_rule();
1499 let mut state = PipelineState::default();
1500 let mut mapping = HashMap::new();
1501 mapping.insert(
1502 "CommandLine".to_string(),
1503 "process.command_line".to_string(),
1504 );
1505 mapping.insert(
1506 "ParentImage".to_string(),
1507 "process.parent.executable".to_string(),
1508 );
1509
1510 let t = Transformation::FieldNameMapping { mapping };
1511 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1512
1513 let det = &rule.detection.named["selection"];
1514 if let Detection::AllOf(items) = det {
1515 assert_eq!(
1516 items[0].field.name,
1517 Some("process.command_line".to_string())
1518 );
1519 assert_eq!(
1520 items[1].field.name,
1521 Some("process.parent.executable".to_string())
1522 );
1523 } else {
1524 panic!("Expected AllOf");
1525 }
1526 }
1527
1528 #[test]
1529 fn test_field_name_prefix() {
1530 let mut rule = make_test_rule();
1531 let mut state = PipelineState::default();
1532 let t = Transformation::FieldNamePrefix {
1533 prefix: "winlog.event_data.".to_string(),
1534 };
1535 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1536
1537 let det = &rule.detection.named["selection"];
1538 if let Detection::AllOf(items) = det {
1539 assert_eq!(
1540 items[0].field.name,
1541 Some("winlog.event_data.CommandLine".to_string())
1542 );
1543 } else {
1544 panic!("Expected AllOf");
1545 }
1546 }
1547
1548 #[test]
1549 fn test_field_name_suffix() {
1550 let mut rule = make_test_rule();
1551 let mut state = PipelineState::default();
1552 let t = Transformation::FieldNameSuffix {
1553 suffix: ".keyword".to_string(),
1554 };
1555 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1556
1557 let det = &rule.detection.named["selection"];
1558 if let Detection::AllOf(items) = det {
1559 assert_eq!(items[0].field.name, Some("CommandLine.keyword".to_string()));
1560 } else {
1561 panic!("Expected AllOf");
1562 }
1563 }
1564
1565 #[test]
1566 fn test_change_logsource() {
1567 let mut rule = make_test_rule();
1568 let mut state = PipelineState::default();
1569 let t = Transformation::ChangeLogsource {
1570 category: Some("endpoint".to_string()),
1571 product: Some("elastic".to_string()),
1572 service: None,
1573 };
1574 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1575
1576 assert_eq!(rule.logsource.category, Some("endpoint".to_string()));
1577 assert_eq!(rule.logsource.product, Some("elastic".to_string()));
1578 }
1579
1580 #[test]
1581 fn test_replace_string() {
1582 let mut rule = make_test_rule();
1583 let mut state = PipelineState::default();
1584 let t = Transformation::ReplaceString {
1585 regex: r"whoami".to_string(),
1586 replacement: "REPLACED".to_string(),
1587 skip_special: false,
1588 };
1589 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1590
1591 let det = &rule.detection.named["selection"];
1592 if let Detection::AllOf(items) = det {
1593 if let SigmaValue::String(s) = &items[0].values[0] {
1594 assert_eq!(s.original, "REPLACED");
1595 } else {
1596 panic!("Expected String value");
1597 }
1598 } else {
1599 panic!("Expected AllOf");
1600 }
1601 }
1602
1603 #[test]
1604 fn test_add_condition() {
1605 let mut rule = make_test_rule();
1606 let mut state = PipelineState::default();
1607 let mut conds = HashMap::new();
1608 conds.insert(
1609 "index".to_string(),
1610 SigmaValue::String(SigmaString::new("windows-*")),
1611 );
1612 let t = Transformation::AddCondition {
1613 conditions: conds,
1614 negated: false,
1615 };
1616 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1617
1618 assert!(
1620 rule.detection
1621 .named
1622 .keys()
1623 .any(|k| k.starts_with("__pipeline_cond_"))
1624 );
1625 assert_eq!(rule.detection.conditions.len(), 1);
1627 if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
1628 assert_eq!(parts.len(), 2);
1629 } else {
1630 panic!("Expected And condition");
1631 }
1632 }
1633
1634 #[test]
1635 fn test_set_state() {
1636 let mut rule = make_test_rule();
1637 let mut state = PipelineState::default();
1638 let t = Transformation::SetState {
1639 key: "index".to_string(),
1640 value: "windows".to_string(),
1641 };
1642 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1643 assert!(state.state_matches("index", "windows"));
1644 }
1645
1646 #[test]
1647 fn test_drop_detection_item_with_field_condition() {
1648 let mut rule = make_test_rule();
1649 let mut state = PipelineState::default();
1650
1651 let field_conds = vec![FieldNameCondition::IncludeFields {
1652 matcher: super::super::conditions::FieldMatcher::Plain(vec!["ParentImage".to_string()]),
1653 }];
1654
1655 let t = Transformation::DropDetectionItem;
1656 t.apply(&mut rule, &mut state, &[], &field_conds, false)
1657 .unwrap();
1658
1659 let det = &rule.detection.named["selection"];
1660 if let Detection::AllOf(items) = det {
1661 assert_eq!(items.len(), 1); assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
1663 } else {
1664 panic!("Expected AllOf");
1665 }
1666 }
1667
1668 #[test]
1669 fn test_field_name_mapping_with_conditions() {
1670 let mut rule = make_test_rule();
1671 let mut state = PipelineState::default();
1672
1673 let field_conds = vec![FieldNameCondition::IncludeFields {
1675 matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
1676 }];
1677
1678 let mut mapping = HashMap::new();
1679 mapping.insert("CommandLine".to_string(), "process.args".to_string());
1680 mapping.insert("ParentImage".to_string(), "process.parent".to_string());
1681
1682 let t = Transformation::FieldNameMapping { mapping };
1683 t.apply(&mut rule, &mut state, &[], &field_conds, false)
1684 .unwrap();
1685
1686 let det = &rule.detection.named["selection"];
1687 if let Detection::AllOf(items) = det {
1688 assert_eq!(items[0].field.name, Some("process.args".to_string()));
1689 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
1691 } else {
1692 panic!("Expected AllOf");
1693 }
1694 }
1695
1696 #[test]
1697 fn test_rule_failure() {
1698 let mut rule = make_test_rule();
1699 let mut state = PipelineState::default();
1700 let t = Transformation::RuleFailure {
1701 message: "Unsupported rule".to_string(),
1702 };
1703 let result = t.apply(&mut rule, &mut state, &[], &[], false);
1704 assert!(result.is_err());
1705 }
1706
1707 #[test]
1708 fn test_value_placeholders() {
1709 let mut named = HashMap::new();
1710 named.insert(
1711 "selection".to_string(),
1712 Detection::AllOf(vec![DetectionItem {
1713 field: FieldSpec::new(Some("User".to_string()), vec![]),
1714 values: vec![SigmaValue::String(SigmaString::new("%admin_users%"))],
1715 }]),
1716 );
1717
1718 let mut rule = SigmaRule {
1719 title: "Test".to_string(),
1720 logsource: LogSource::default(),
1721 detection: Detections {
1722 named,
1723 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
1724 condition_strings: vec!["selection".to_string()],
1725 timeframe: None,
1726 },
1727 id: None,
1728 name: None,
1729 related: vec![],
1730 taxonomy: None,
1731 status: None,
1732 description: None,
1733 license: None,
1734 author: None,
1735 references: vec![],
1736 date: None,
1737 modified: None,
1738 fields: vec![],
1739 falsepositives: vec![],
1740 level: None,
1741 tags: vec![],
1742 scope: vec![],
1743 custom_attributes: HashMap::new(),
1744 };
1745
1746 let mut state = PipelineState::default();
1747 state.vars.insert(
1748 "admin_users".to_string(),
1749 vec!["root".to_string(), "admin".to_string()],
1750 );
1751
1752 let t = Transformation::ValuePlaceholders;
1753 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1754
1755 let det = &rule.detection.named["selection"];
1756 if let Detection::AllOf(items) = det {
1757 assert_eq!(items[0].values.len(), 2);
1759 } else {
1760 panic!("Expected AllOf");
1761 }
1762 }
1763
1764 #[test]
1765 fn test_field_name_transform_lowercase() {
1766 let mut rule = make_test_rule();
1767 let mut state = PipelineState::default();
1768 let t = Transformation::FieldNameTransform {
1769 transform_func: "lower".to_string(),
1770 mapping: HashMap::new(),
1771 };
1772 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1773
1774 let det = &rule.detection.named["selection"];
1775 if let Detection::AllOf(items) = det {
1776 assert_eq!(items[0].field.name, Some("commandline".to_string()));
1777 assert_eq!(items[1].field.name, Some("parentimage".to_string()));
1778 } else {
1779 panic!("Expected AllOf");
1780 }
1781 }
1782
1783 #[test]
1784 fn test_field_name_transform_with_mapping_override() {
1785 let mut rule = make_test_rule();
1786 let mut state = PipelineState::default();
1787 let mut mapping = HashMap::new();
1788 mapping.insert("CommandLine".to_string(), "cmd_line".to_string());
1789 let t = Transformation::FieldNameTransform {
1790 transform_func: "lower".to_string(),
1791 mapping,
1792 };
1793 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1794
1795 let det = &rule.detection.named["selection"];
1796 if let Detection::AllOf(items) = det {
1797 assert_eq!(items[0].field.name, Some("cmd_line".to_string()));
1799 assert_eq!(items[1].field.name, Some("parentimage".to_string()));
1801 } else {
1802 panic!("Expected AllOf");
1803 }
1804 }
1805
1806 #[test]
1807 fn test_field_name_transform_snake_case() {
1808 let mut rule = make_test_rule();
1809 let mut state = PipelineState::default();
1810 let t = Transformation::FieldNameTransform {
1811 transform_func: "snake_case".to_string(),
1812 mapping: HashMap::new(),
1813 };
1814 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1815
1816 let det = &rule.detection.named["selection"];
1817 if let Detection::AllOf(items) = det {
1818 assert_eq!(items[0].field.name, Some("command_line".to_string()));
1819 assert_eq!(items[1].field.name, Some("parent_image".to_string()));
1820 } else {
1821 panic!("Expected AllOf");
1822 }
1823 }
1824
1825 #[test]
1826 fn test_hashes_fields_decomposition() {
1827 let mut named = HashMap::new();
1828 named.insert(
1829 "selection".to_string(),
1830 Detection::AllOf(vec![DetectionItem {
1831 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
1832 values: vec![SigmaValue::String(SigmaString::new(
1833 "SHA1=abc123,MD5=def456",
1834 ))],
1835 }]),
1836 );
1837
1838 let mut rule = make_test_rule();
1839 rule.detection.named = named;
1840
1841 let mut state = PipelineState::default();
1842 let t = Transformation::HashesFields {
1843 valid_hash_algos: vec!["SHA1".to_string(), "MD5".to_string()],
1844 field_prefix: "File".to_string(),
1845 drop_algo_prefix: false,
1846 };
1847 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1848
1849 let det = &rule.detection.named["selection"];
1850 if let Detection::AllOf(items) = det {
1851 assert_eq!(items.len(), 2);
1852 assert_eq!(items[0].field.name, Some("FileSHA1".to_string()));
1853 assert_eq!(items[1].field.name, Some("FileMD5".to_string()));
1854 if let SigmaValue::String(s) = &items[0].values[0] {
1855 assert_eq!(s.original, "abc123");
1856 }
1857 if let SigmaValue::String(s) = &items[1].values[0] {
1858 assert_eq!(s.original, "def456");
1859 }
1860 } else {
1861 panic!("Expected AllOf");
1862 }
1863 }
1864
1865 #[test]
1866 fn test_map_string() {
1867 let mut rule = make_test_rule();
1868 let mut state = PipelineState::default();
1869 let mut mapping = HashMap::new();
1870 mapping.insert("whoami".to_string(), vec!["who_am_i".to_string()]);
1871 let t = Transformation::MapString { mapping };
1872 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1873
1874 let det = &rule.detection.named["selection"];
1875 if let Detection::AllOf(items) = det {
1876 if let SigmaValue::String(s) = &items[0].values[0] {
1877 assert_eq!(s.original, "who_am_i");
1878 } else {
1879 panic!("Expected String value");
1880 }
1881 } else {
1882 panic!("Expected AllOf");
1883 }
1884 }
1885
1886 #[test]
1887 fn test_map_string_no_match() {
1888 let mut rule = make_test_rule();
1889 let mut state = PipelineState::default();
1890 let mut mapping = HashMap::new();
1891 mapping.insert("nonexistent".to_string(), vec!["replaced".to_string()]);
1892 let t = Transformation::MapString { mapping };
1893 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1894
1895 let det = &rule.detection.named["selection"];
1897 if let Detection::AllOf(items) = det
1898 && let SigmaValue::String(s) = &items[0].values[0]
1899 {
1900 assert_eq!(s.original, "whoami");
1901 }
1902 }
1903
1904 #[test]
1905 fn test_set_value() {
1906 let mut rule = make_test_rule();
1907 let mut state = PipelineState::default();
1908 let t = Transformation::SetValue {
1909 value: SigmaValue::String(SigmaString::new("FIXED")),
1910 };
1911 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1912
1913 let det = &rule.detection.named["selection"];
1914 if let Detection::AllOf(items) = det {
1915 for item in items {
1916 assert_eq!(item.values.len(), 1);
1917 if let SigmaValue::String(s) = &item.values[0] {
1918 assert_eq!(s.original, "FIXED");
1919 }
1920 }
1921 } else {
1922 panic!("Expected AllOf");
1923 }
1924 }
1925
1926 #[test]
1927 fn test_convert_type_string_to_int() {
1928 let mut named = HashMap::new();
1929 named.insert(
1930 "selection".to_string(),
1931 Detection::AllOf(vec![DetectionItem {
1932 field: FieldSpec::new(Some("EventID".to_string()), vec![]),
1933 values: vec![SigmaValue::String(SigmaString::new("4688"))],
1934 }]),
1935 );
1936 let mut rule = make_test_rule();
1937 rule.detection.named = named;
1938
1939 let mut state = PipelineState::default();
1940 let t = Transformation::ConvertType {
1941 target_type: "int".to_string(),
1942 };
1943 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1944
1945 let det = &rule.detection.named["selection"];
1946 if let Detection::AllOf(items) = det {
1947 assert!(matches!(items[0].values[0], SigmaValue::Integer(4688)));
1948 } else {
1949 panic!("Expected AllOf");
1950 }
1951 }
1952
1953 #[test]
1954 fn test_convert_type_int_to_string() {
1955 let mut named = HashMap::new();
1956 named.insert(
1957 "selection".to_string(),
1958 Detection::AllOf(vec![DetectionItem {
1959 field: FieldSpec::new(Some("EventID".to_string()), vec![]),
1960 values: vec![SigmaValue::Integer(4688)],
1961 }]),
1962 );
1963 let mut rule = make_test_rule();
1964 rule.detection.named = named;
1965
1966 let mut state = PipelineState::default();
1967 let t = Transformation::ConvertType {
1968 target_type: "str".to_string(),
1969 };
1970 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1971
1972 let det = &rule.detection.named["selection"];
1973 if let Detection::AllOf(items) = det {
1974 if let SigmaValue::String(s) = &items[0].values[0] {
1975 assert_eq!(s.original, "4688");
1976 } else {
1977 panic!("Expected String");
1978 }
1979 }
1980 }
1981
1982 #[test]
1983 fn test_convert_type_to_bool() {
1984 let mut named = HashMap::new();
1985 named.insert(
1986 "selection".to_string(),
1987 Detection::AllOf(vec![DetectionItem {
1988 field: FieldSpec::new(Some("Enabled".to_string()), vec![]),
1989 values: vec![SigmaValue::String(SigmaString::new("true"))],
1990 }]),
1991 );
1992 let mut rule = make_test_rule();
1993 rule.detection.named = named;
1994
1995 let mut state = PipelineState::default();
1996 let t = Transformation::ConvertType {
1997 target_type: "bool".to_string(),
1998 };
1999 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2000
2001 let det = &rule.detection.named["selection"];
2002 if let Detection::AllOf(items) = det {
2003 assert!(matches!(items[0].values[0], SigmaValue::Bool(true)));
2004 }
2005 }
2006
2007 #[test]
2008 fn test_regex_noop() {
2009 let mut rule = make_test_rule();
2010 let mut state = PipelineState::default();
2011 let t = Transformation::Regex;
2012 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2013 assert!(!result); }
2015
2016 #[test]
2017 fn test_add_field() {
2018 let mut rule = make_test_rule();
2019 assert!(rule.fields.is_empty());
2020
2021 let mut state = PipelineState::default();
2022 let t = Transformation::AddField {
2023 field: "EventID".to_string(),
2024 };
2025 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2026 assert_eq!(rule.fields, vec!["EventID".to_string()]);
2027
2028 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2030 assert_eq!(rule.fields, vec!["EventID".to_string()]);
2031 }
2032
2033 #[test]
2034 fn test_remove_field() {
2035 let mut rule = make_test_rule();
2036 rule.fields = vec!["EventID".to_string(), "CommandLine".to_string()];
2037
2038 let mut state = PipelineState::default();
2039 let t = Transformation::RemoveField {
2040 field: "EventID".to_string(),
2041 };
2042 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2043 assert_eq!(rule.fields, vec!["CommandLine".to_string()]);
2044 }
2045
2046 #[test]
2047 fn test_set_field() {
2048 let mut rule = make_test_rule();
2049 rule.fields = vec!["old".to_string()];
2050
2051 let mut state = PipelineState::default();
2052 let t = Transformation::SetField {
2053 fields: vec!["new1".to_string(), "new2".to_string()],
2054 };
2055 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2056 assert_eq!(rule.fields, vec!["new1".to_string(), "new2".to_string()]);
2057 }
2058
2059 #[test]
2060 fn test_set_custom_attribute() {
2061 let mut rule = make_test_rule();
2062 let mut state = PipelineState::default();
2063 let t = Transformation::SetCustomAttribute {
2064 attribute: "custom.key".to_string(),
2065 value: "custom_value".to_string(),
2066 };
2067 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2068 assert!(result);
2069 assert_eq!(
2070 rule.custom_attributes
2071 .get("custom.key")
2072 .and_then(|v| v.as_str()),
2073 Some("custom_value")
2074 );
2075 }
2076
2077 #[test]
2078 fn test_case_transformation_lower() {
2079 let mut rule = make_test_rule();
2080 let mut state = PipelineState::default();
2081 let t = Transformation::CaseTransformation {
2082 case_type: "lower".to_string(),
2083 };
2084 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2085
2086 let det = &rule.detection.named["selection"];
2087 if let Detection::AllOf(items) = det {
2088 if let SigmaValue::String(s) = &items[0].values[0] {
2090 assert_eq!(s.original, "whoami");
2091 }
2092 if let SigmaValue::String(s) = &items[1].values[0] {
2094 assert_eq!(s.original, "\\cmd.exe");
2095 }
2096 }
2097 }
2098
2099 #[test]
2100 fn test_case_transformation_upper() {
2101 let mut rule = make_test_rule();
2102 let mut state = PipelineState::default();
2103 let t = Transformation::CaseTransformation {
2104 case_type: "upper".to_string(),
2105 };
2106 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2107
2108 let det = &rule.detection.named["selection"];
2109 if let Detection::AllOf(items) = det {
2110 if let SigmaValue::String(s) = &items[0].values[0] {
2111 assert_eq!(s.original, "WHOAMI");
2112 }
2113 if let SigmaValue::String(s) = &items[1].values[0] {
2114 assert_eq!(s.original, "\\CMD.EXE");
2115 }
2116 }
2117 }
2118
2119 #[test]
2120 fn test_nest_transformation() {
2121 let mut rule = make_test_rule();
2122 let mut state = PipelineState::default();
2123
2124 let items = vec![
2126 super::super::TransformationItem {
2127 id: Some("inner_prefix".to_string()),
2128 transformation: Transformation::FieldNamePrefix {
2129 prefix: "winlog.".to_string(),
2130 },
2131 rule_conditions: vec![],
2132 rule_cond_expr: None,
2133 detection_item_conditions: vec![],
2134 field_name_conditions: vec![],
2135 field_name_cond_not: false,
2136 },
2137 super::super::TransformationItem {
2138 id: Some("inner_suffix".to_string()),
2139 transformation: Transformation::FieldNameSuffix {
2140 suffix: ".keyword".to_string(),
2141 },
2142 rule_conditions: vec![],
2143 rule_cond_expr: None,
2144 detection_item_conditions: vec![],
2145 field_name_conditions: vec![],
2146 field_name_cond_not: false,
2147 },
2148 ];
2149
2150 let t = Transformation::Nest { items };
2151 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2152
2153 let det = &rule.detection.named["selection"];
2154 if let Detection::AllOf(items) = det {
2155 assert_eq!(
2156 items[0].field.name,
2157 Some("winlog.CommandLine.keyword".to_string())
2158 );
2159 assert_eq!(
2160 items[1].field.name,
2161 Some("winlog.ParentImage.keyword".to_string())
2162 );
2163 } else {
2164 panic!("Expected AllOf");
2165 }
2166
2167 assert!(state.was_applied("inner_prefix"));
2169 assert!(state.was_applied("inner_suffix"));
2170 }
2171
2172 #[test]
2177 fn test_field_name_prefix_mapping() {
2178 let mut rule = make_test_rule();
2179 let mut state = PipelineState::default();
2180 let mut mapping = HashMap::new();
2181 mapping.insert("Command".to_string(), "process.".to_string());
2182 mapping.insert("Parent".to_string(), "process.parent.".to_string());
2183
2184 let t = Transformation::FieldNamePrefixMapping { mapping };
2185 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2186
2187 let det = &rule.detection.named["selection"];
2188 if let Detection::AllOf(items) = det {
2189 assert_eq!(items[0].field.name, Some("process.Line".to_string()));
2191 assert_eq!(
2193 items[1].field.name,
2194 Some("process.parent.Image".to_string())
2195 );
2196 } else {
2197 panic!("Expected AllOf");
2198 }
2199 }
2200
2201 #[test]
2202 fn test_field_name_prefix_mapping_no_match() {
2203 let mut rule = make_test_rule();
2204 let mut state = PipelineState::default();
2205 let mut mapping = HashMap::new();
2206 mapping.insert("NoMatch".to_string(), "replaced.".to_string());
2207
2208 let t = Transformation::FieldNamePrefixMapping { mapping };
2209 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2210
2211 let det = &rule.detection.named["selection"];
2213 if let Detection::AllOf(items) = det {
2214 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2215 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
2216 } else {
2217 panic!("Expected AllOf");
2218 }
2219 }
2220
2221 #[test]
2222 fn test_wildcard_placeholders_replaces_unresolved() {
2223 let mut named = HashMap::new();
2224 named.insert(
2225 "selection".to_string(),
2226 Detection::AllOf(vec![DetectionItem {
2227 field: FieldSpec::new(Some("User".to_string()), vec![]),
2228 values: vec![SigmaValue::String(SigmaString::new("%unknown_var%"))],
2229 }]),
2230 );
2231
2232 let mut rule = SigmaRule {
2233 title: "Test".to_string(),
2234 logsource: LogSource::default(),
2235 detection: Detections {
2236 named,
2237 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2238 condition_strings: vec!["selection".to_string()],
2239 timeframe: None,
2240 },
2241 id: None,
2242 name: None,
2243 related: vec![],
2244 taxonomy: None,
2245 status: None,
2246 description: None,
2247 license: None,
2248 author: None,
2249 references: vec![],
2250 date: None,
2251 modified: None,
2252 fields: vec![],
2253 falsepositives: vec![],
2254 level: None,
2255 tags: vec![],
2256 scope: vec![],
2257 custom_attributes: HashMap::new(),
2258 };
2259
2260 let mut state = PipelineState::default();
2261 let t = Transformation::WildcardPlaceholders;
2263 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2264
2265 let det = &rule.detection.named["selection"];
2266 if let Detection::AllOf(items) = det {
2267 if let SigmaValue::String(s) = &items[0].values[0] {
2268 assert_eq!(s.original, "*", "unresolved placeholder should become *");
2269 } else {
2270 panic!("Expected String value");
2271 }
2272 } else {
2273 panic!("Expected AllOf");
2274 }
2275 }
2276
2277 #[test]
2278 fn test_wildcard_placeholders_with_known_var() {
2279 let mut named = HashMap::new();
2280 named.insert(
2281 "selection".to_string(),
2282 Detection::AllOf(vec![DetectionItem {
2283 field: FieldSpec::new(Some("User".to_string()), vec![]),
2284 values: vec![SigmaValue::String(SigmaString::new("%admin%"))],
2285 }]),
2286 );
2287
2288 let mut rule = SigmaRule {
2289 title: "Test".to_string(),
2290 logsource: LogSource::default(),
2291 detection: Detections {
2292 named,
2293 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2294 condition_strings: vec!["selection".to_string()],
2295 timeframe: None,
2296 },
2297 id: None,
2298 name: None,
2299 related: vec![],
2300 taxonomy: None,
2301 status: None,
2302 description: None,
2303 license: None,
2304 author: None,
2305 references: vec![],
2306 date: None,
2307 modified: None,
2308 fields: vec![],
2309 falsepositives: vec![],
2310 level: None,
2311 tags: vec![],
2312 scope: vec![],
2313 custom_attributes: HashMap::new(),
2314 };
2315
2316 let mut state = PipelineState::default();
2317 state
2318 .vars
2319 .insert("admin".to_string(), vec!["root".to_string()]);
2320
2321 let t = Transformation::WildcardPlaceholders;
2323 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2324
2325 let det = &rule.detection.named["selection"];
2326 if let Detection::AllOf(items) = det {
2327 if let SigmaValue::String(s) = &items[0].values[0] {
2328 assert_eq!(s.original, "root");
2329 } else {
2330 panic!("Expected String value");
2331 }
2332 } else {
2333 panic!("Expected AllOf");
2334 }
2335 }
2336
2337 #[test]
2338 fn test_detection_item_failure_fires_on_match() {
2339 let mut rule = make_test_rule();
2340 let mut state = PipelineState::default();
2341
2342 let det_conds = vec![DetectionItemCondition::MatchString {
2344 regex: regex::Regex::new("whoami").unwrap(),
2345 negate: false,
2346 }];
2347
2348 let t = Transformation::DetectionItemFailure {
2349 message: "Unsupported detection item".to_string(),
2350 };
2351 let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
2352 assert!(result.is_err());
2353 let err = result.unwrap_err().to_string();
2354 assert!(err.contains("Unsupported detection item"));
2355 }
2356
2357 #[test]
2358 fn test_detection_item_failure_skips_on_no_match() {
2359 let mut rule = make_test_rule();
2360 let mut state = PipelineState::default();
2361
2362 let det_conds = vec![DetectionItemCondition::MatchString {
2364 regex: regex::Regex::new("nonexistent_value").unwrap(),
2365 negate: false,
2366 }];
2367
2368 let t = Transformation::DetectionItemFailure {
2369 message: "Should not fire".to_string(),
2370 };
2371 let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
2372 assert!(result.is_ok());
2373 assert!(!result.unwrap()); }
2375
2376 #[test]
2377 fn test_query_expression_placeholders_noop() {
2378 let mut rule = make_test_rule();
2379 let mut state = PipelineState::default();
2380 let t = Transformation::QueryExpressionPlaceholders {
2381 expression: "{field}={value}".to_string(),
2382 };
2383 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2384 assert!(!result); }
2386
2387 #[test]
2392 fn test_add_condition_negated() {
2393 let mut rule = make_test_rule();
2394 let mut state = PipelineState::default();
2395 let mut conds = HashMap::new();
2396 conds.insert(
2397 "User".to_string(),
2398 SigmaValue::String(SigmaString::new("SYSTEM")),
2399 );
2400 let t = Transformation::AddCondition {
2401 conditions: conds,
2402 negated: true,
2403 };
2404 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2405
2406 assert_eq!(rule.detection.conditions.len(), 1);
2408 if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
2409 assert_eq!(parts.len(), 2);
2410 assert!(
2412 matches!(&parts[1], ConditionExpr::Not(_)),
2413 "Expected negated condition, got: {:?}",
2414 parts[1]
2415 );
2416 } else {
2417 panic!("Expected And condition");
2418 }
2419 }
2420
2421 #[test]
2426 fn test_replace_string_with_detection_item_condition() {
2427 let mut rule = make_test_rule();
2428 let mut state = PipelineState::default();
2429
2430 let det_conds = vec![DetectionItemCondition::MatchString {
2432 regex: regex::Regex::new("whoami").unwrap(),
2433 negate: false,
2434 }];
2435
2436 let t = Transformation::ReplaceString {
2437 regex: r"whoami".to_string(),
2438 replacement: "REPLACED".to_string(),
2439 skip_special: false,
2440 };
2441 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2442 .unwrap();
2443
2444 let det = &rule.detection.named["selection"];
2445 if let Detection::AllOf(items) = det {
2446 if let SigmaValue::String(s) = &items[0].values[0] {
2448 assert_eq!(s.original, "REPLACED");
2449 }
2450 if let SigmaValue::String(s) = &items[1].values[0] {
2452 assert_eq!(s.original, "\\cmd.exe");
2453 }
2454 } else {
2455 panic!("Expected AllOf");
2456 }
2457 }
2458
2459 #[test]
2460 fn test_set_value_with_is_null_condition() {
2461 let mut named = HashMap::new();
2463 named.insert(
2464 "selection".to_string(),
2465 Detection::AllOf(vec![
2466 DetectionItem {
2467 field: FieldSpec::new(Some("FieldA".to_string()), vec![]),
2468 values: vec![SigmaValue::Null],
2469 },
2470 DetectionItem {
2471 field: FieldSpec::new(Some("FieldB".to_string()), vec![]),
2472 values: vec![SigmaValue::String(SigmaString::new("value"))],
2473 },
2474 ]),
2475 );
2476
2477 let mut rule = make_test_rule();
2478 rule.detection.named = named;
2479 let mut state = PipelineState::default();
2480
2481 let det_conds = vec![DetectionItemCondition::IsNull { negate: false }];
2483
2484 let t = Transformation::SetValue {
2485 value: SigmaValue::String(SigmaString::new("DEFAULT")),
2486 };
2487 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2488 .unwrap();
2489
2490 let det = &rule.detection.named["selection"];
2491 if let Detection::AllOf(items) = det {
2492 if let SigmaValue::String(s) = &items[0].values[0] {
2494 assert_eq!(s.original, "DEFAULT");
2495 } else {
2496 panic!("Expected String after set_value on null");
2497 }
2498 if let SigmaValue::String(s) = &items[1].values[0] {
2500 assert_eq!(s.original, "value");
2501 }
2502 } else {
2503 panic!("Expected AllOf");
2504 }
2505 }
2506
2507 #[test]
2508 fn test_drop_detection_item_with_match_string_condition() {
2509 let mut rule = make_test_rule();
2510 let mut state = PipelineState::default();
2511
2512 let det_conds = vec![DetectionItemCondition::MatchString {
2514 regex: regex::Regex::new("whoami").unwrap(),
2515 negate: false,
2516 }];
2517
2518 let t = Transformation::DropDetectionItem;
2519 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2520 .unwrap();
2521
2522 let det = &rule.detection.named["selection"];
2523 if let Detection::AllOf(items) = det {
2524 assert_eq!(items.len(), 1);
2525 assert_eq!(items[0].field.name, Some("ParentImage".to_string()));
2527 } else {
2528 panic!("Expected AllOf");
2529 }
2530 }
2531
2532 #[test]
2537 fn test_field_name_mapping_with_cond_not() {
2538 let mut rule = make_test_rule();
2539 let mut state = PipelineState::default();
2540
2541 let field_conds = vec![FieldNameCondition::IncludeFields {
2543 matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
2544 }];
2545
2546 let mut mapping = HashMap::new();
2547 mapping.insert("CommandLine".to_string(), "cmd".to_string());
2548 mapping.insert("ParentImage".to_string(), "parent".to_string());
2549
2550 let t = Transformation::FieldNameMapping { mapping };
2551 t.apply(&mut rule, &mut state, &[], &field_conds, true)
2553 .unwrap();
2554
2555 let det = &rule.detection.named["selection"];
2556 if let Detection::AllOf(items) = det {
2557 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2559 assert_eq!(items[1].field.name, Some("parent".to_string()));
2561 } else {
2562 panic!("Expected AllOf");
2563 }
2564 }
2565
2566 #[test]
2571 fn test_field_name_mapping_empty() {
2572 let mut rule = make_test_rule();
2573 let mut state = PipelineState::default();
2574 let t = Transformation::FieldNameMapping {
2575 mapping: HashMap::new(),
2576 };
2577 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2578
2579 let det = &rule.detection.named["selection"];
2581 if let Detection::AllOf(items) = det {
2582 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2583 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
2584 } else {
2585 panic!("Expected AllOf");
2586 }
2587 }
2588
2589 #[test]
2590 fn test_field_name_prefix_mapping_empty() {
2591 let mut rule = make_test_rule();
2592 let mut state = PipelineState::default();
2593 let t = Transformation::FieldNamePrefixMapping {
2594 mapping: HashMap::new(),
2595 };
2596 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2597
2598 let det = &rule.detection.named["selection"];
2599 if let Detection::AllOf(items) = det {
2600 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2601 } else {
2602 panic!("Expected AllOf");
2603 }
2604 }
2605
2606 #[test]
2607 fn test_map_string_empty_mapping() {
2608 let mut rule = make_test_rule();
2609 let mut state = PipelineState::default();
2610 let t = Transformation::MapString {
2611 mapping: HashMap::new(),
2612 };
2613 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2614
2615 let det = &rule.detection.named["selection"];
2616 if let Detection::AllOf(items) = det
2617 && let SigmaValue::String(s) = &items[0].values[0]
2618 {
2619 assert_eq!(s.original, "whoami");
2620 }
2621 }
2622
2623 #[test]
2624 fn test_hashes_fields_empty_algos() {
2625 let mut named = HashMap::new();
2627 named.insert(
2628 "selection".to_string(),
2629 Detection::AllOf(vec![DetectionItem {
2630 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2631 values: vec![SigmaValue::String(SigmaString::new(
2632 "SHA256=abc123,IMPHASH=def456",
2633 ))],
2634 }]),
2635 );
2636
2637 let mut rule = make_test_rule();
2638 rule.detection.named = named;
2639
2640 let mut state = PipelineState::default();
2641 let t = Transformation::HashesFields {
2642 valid_hash_algos: vec![], field_prefix: "File".to_string(),
2644 drop_algo_prefix: false,
2645 };
2646 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2647
2648 let det = &rule.detection.named["selection"];
2649 if let Detection::AllOf(items) = det {
2650 assert_eq!(items.len(), 2);
2651 assert_eq!(items[0].field.name, Some("FileSHA256".to_string()));
2652 assert_eq!(items[1].field.name, Some("FileIMPHASH".to_string()));
2653 } else {
2654 panic!("Expected AllOf");
2655 }
2656 }
2657
2658 #[test]
2659 fn test_hashes_fields_drop_algo_prefix() {
2660 let mut named = HashMap::new();
2661 named.insert(
2662 "selection".to_string(),
2663 Detection::AllOf(vec![DetectionItem {
2664 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2665 values: vec![SigmaValue::String(SigmaString::new("MD5=abc123"))],
2666 }]),
2667 );
2668
2669 let mut rule = make_test_rule();
2670 rule.detection.named = named;
2671 let mut state = PipelineState::default();
2672
2673 let t = Transformation::HashesFields {
2674 valid_hash_algos: vec!["MD5".to_string()],
2675 field_prefix: "Hash".to_string(),
2676 drop_algo_prefix: true,
2677 };
2678 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2679
2680 let det = &rule.detection.named["selection"];
2681 if let Detection::AllOf(items) = det {
2682 assert_eq!(items.len(), 1);
2683 assert_eq!(items[0].field.name, Some("Hash".to_string()));
2685 } else {
2686 panic!("Expected AllOf");
2687 }
2688 }
2689
2690 #[test]
2695 fn test_replace_string_invalid_regex() {
2696 let mut rule = make_test_rule();
2697 let mut state = PipelineState::default();
2698 let t = Transformation::ReplaceString {
2699 regex: r"[invalid".to_string(), replacement: "x".to_string(),
2701 skip_special: false,
2702 };
2703 let result = t.apply(&mut rule, &mut state, &[], &[], false);
2704 assert!(result.is_err());
2705 let err = result.unwrap_err().to_string();
2706 assert!(
2707 err.contains("bad regex"),
2708 "error should mention regex: {err}"
2709 );
2710 }
2711
2712 #[test]
2717 fn test_case_transformation_with_negated_match_string() {
2718 let mut rule = make_test_rule();
2719 let mut state = PipelineState::default();
2720
2721 let det_conds = vec![DetectionItemCondition::MatchString {
2723 regex: regex::Regex::new("whoami").unwrap(),
2724 negate: true,
2725 }];
2726
2727 let t = Transformation::CaseTransformation {
2728 case_type: "upper".to_string(),
2729 };
2730 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2731 .unwrap();
2732
2733 let det = &rule.detection.named["selection"];
2734 if let Detection::AllOf(items) = det {
2735 if let SigmaValue::String(s) = &items[0].values[0] {
2737 assert_eq!(s.original, "whoami");
2738 }
2739 if let SigmaValue::String(s) = &items[1].values[0] {
2741 assert_eq!(s.original, "\\CMD.EXE");
2742 }
2743 } else {
2744 panic!("Expected AllOf");
2745 }
2746 }
2747
2748 #[test]
2753 fn test_multi_transformation_chaining_pipeline() {
2754 use crate::pipeline::parse_pipeline;
2755
2756 let yaml = r#"
2757name: Multi-step Pipeline
2758transformations:
2759 - id: step1_map
2760 type: field_name_mapping
2761 mapping:
2762 CommandLine: process.command_line
2763 ParentImage: process.parent.executable
2764 - id: step2_prefix
2765 type: field_name_prefix
2766 prefix: "winlog."
2767 rule_conditions:
2768 - type: logsource
2769 product: windows
2770 - id: step3_case
2771 type: case_transformation
2772 case_type: upper
2773 field_name_conditions:
2774 - type: include_fields
2775 fields:
2776 - winlog.process.command_line
2777 - id: step4_attr
2778 type: set_custom_attribute
2779 attribute: rsigma.processed
2780 value: "true"
2781"#;
2782 let pipeline = parse_pipeline(yaml).unwrap();
2783
2784 let mut rule = make_test_rule(); let mut state = PipelineState::new(pipeline.vars.clone());
2786 pipeline.apply(&mut rule, &mut state).unwrap();
2787
2788 let det = &rule.detection.named["selection"];
2789 if let Detection::AllOf(items) = det {
2790 assert_eq!(
2793 items[0].field.name,
2794 Some("winlog.process.command_line".to_string())
2795 );
2796 if let SigmaValue::String(s) = &items[0].values[0] {
2798 assert_eq!(s.original, "WHOAMI");
2799 }
2800
2801 assert_eq!(
2803 items[1].field.name,
2804 Some("winlog.process.parent.executable".to_string())
2805 );
2806 if let SigmaValue::String(s) = &items[1].values[0] {
2808 assert_eq!(s.original, "\\cmd.exe");
2809 }
2810 } else {
2811 panic!("Expected AllOf");
2812 }
2813
2814 assert_eq!(
2816 rule.custom_attributes
2817 .get("rsigma.processed")
2818 .and_then(|v| v.as_str()),
2819 Some("true")
2820 );
2821
2822 assert!(state.was_applied("step1_map"));
2824 assert!(state.was_applied("step2_prefix"));
2825 assert!(state.was_applied("step3_case"));
2826 assert!(state.was_applied("step4_attr"));
2827 }
2828
2829 #[test]
2834 fn test_map_string_one_to_many() {
2835 let mut rule = make_test_rule();
2836 let mut state = PipelineState::default();
2837 let mut mapping = HashMap::new();
2838 mapping.insert(
2839 "whoami".to_string(),
2840 vec!["who".to_string(), "am_i".to_string(), "test".to_string()],
2841 );
2842 let t = Transformation::MapString { mapping };
2843 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2844
2845 let det = &rule.detection.named["selection"];
2846 if let Detection::AllOf(items) = det {
2847 assert_eq!(items[0].values.len(), 3);
2849 if let SigmaValue::String(s) = &items[0].values[0] {
2850 assert_eq!(s.original, "who");
2851 }
2852 if let SigmaValue::String(s) = &items[0].values[1] {
2853 assert_eq!(s.original, "am_i");
2854 }
2855 if let SigmaValue::String(s) = &items[0].values[2] {
2856 assert_eq!(s.original, "test");
2857 }
2858 } else {
2859 panic!("Expected AllOf");
2860 }
2861 }
2862
2863 #[test]
2864 fn test_map_string_one_to_many_mixed() {
2865 let yaml = r#"
2867title: Test Rule
2868logsource:
2869 product: windows
2870detection:
2871 selection:
2872 CommandLine:
2873 - whoami
2874 - ipconfig
2875 condition: selection
2876level: medium
2877"#;
2878 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
2879 let mut rule = collection.rules[0].clone();
2880 let mut state = PipelineState::default();
2881 let mut mapping = HashMap::new();
2882 mapping.insert(
2883 "whoami".to_string(),
2884 vec!["who".to_string(), "am_i".to_string()],
2885 );
2886 let t = Transformation::MapString { mapping };
2888 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2889
2890 let det = &rule.detection.named["selection"];
2891 if let Detection::AllOf(items) = det {
2892 assert_eq!(items[0].values.len(), 3);
2894 if let SigmaValue::String(s) = &items[0].values[0] {
2895 assert_eq!(s.original, "who");
2896 }
2897 if let SigmaValue::String(s) = &items[0].values[1] {
2898 assert_eq!(s.original, "am_i");
2899 }
2900 if let SigmaValue::String(s) = &items[0].values[2] {
2901 assert_eq!(s.original, "ipconfig");
2902 }
2903 } else {
2904 panic!("Expected AllOf");
2905 }
2906 }
2907
2908 #[test]
2913 fn test_replace_string_skip_special_preserves_wildcards() {
2914 let yaml = r#"
2916title: Test Rule
2917logsource:
2918 product: windows
2919detection:
2920 selection:
2921 CommandLine: '*whoami*'
2922 condition: selection
2923level: medium
2924"#;
2925 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
2926 let mut rule = collection.rules[0].clone();
2927 let mut state = PipelineState::default();
2928 let t = Transformation::ReplaceString {
2929 regex: r"whoami".to_string(),
2930 replacement: "REPLACED".to_string(),
2931 skip_special: true,
2932 };
2933 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2934
2935 let det = &rule.detection.named["selection"];
2936 if let Detection::AllOf(items) = det {
2937 let s = match &items[0].values[0] {
2938 SigmaValue::String(s) => s,
2939 _ => panic!("Expected String"),
2940 };
2941 assert!(s.contains_wildcards(), "Wildcards should be preserved");
2943 assert!(
2944 s.original.contains("REPLACED"),
2945 "Plain part should be replaced, got: {}",
2946 s.original
2947 );
2948 assert!(
2949 !s.original.contains("whoami"),
2950 "Original text should be gone"
2951 );
2952 } else {
2953 panic!("Expected AllOf");
2954 }
2955 }
2956
2957 #[test]
2958 fn test_replace_string_skip_special_false_replaces_whole() {
2959 let yaml = r#"
2961title: Test Rule
2962logsource:
2963 product: windows
2964detection:
2965 selection:
2966 CommandLine: '*whoami*'
2967 condition: selection
2968level: medium
2969"#;
2970 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
2971 let mut rule = collection.rules[0].clone();
2972 let mut state = PipelineState::default();
2973 let t = Transformation::ReplaceString {
2974 regex: r"\*".to_string(),
2975 replacement: "STAR".to_string(),
2976 skip_special: false,
2977 };
2978 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2979
2980 let det = &rule.detection.named["selection"];
2981 if let Detection::AllOf(items) = det {
2982 let s = match &items[0].values[0] {
2983 SigmaValue::String(s) => s,
2984 _ => panic!("Expected String"),
2985 };
2986 assert!(
2988 s.original.contains("STAR"),
2989 "Wildcards in original should be replaced as text, got: {}",
2990 s.original
2991 );
2992 } else {
2993 panic!("Expected AllOf");
2994 }
2995 }
2996
2997 #[test]
2998 fn test_replace_string_skip_special_plain_string() {
2999 let mut rule = make_test_rule();
3001 let mut state = PipelineState::default();
3002 let t = Transformation::ReplaceString {
3003 regex: r"whoami".to_string(),
3004 replacement: "REPLACED".to_string(),
3005 skip_special: true,
3006 };
3007 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3008
3009 let det = &rule.detection.named["selection"];
3010 if let Detection::AllOf(items) = det
3011 && let SigmaValue::String(s) = &items[0].values[0]
3012 {
3013 assert_eq!(s.original, "REPLACED");
3014 }
3015 }
3016
3017 #[test]
3022 fn test_case_transformation_snake_case() {
3023 let yaml = r#"
3024title: Test Rule
3025logsource:
3026 product: windows
3027detection:
3028 selection:
3029 CommandLine: CommandAndControl
3030 condition: selection
3031level: medium
3032"#;
3033 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
3034 let mut rule = collection.rules[0].clone();
3035 let mut state = PipelineState::default();
3036 let t = Transformation::CaseTransformation {
3037 case_type: "snake_case".to_string(),
3038 };
3039 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3040
3041 let det = &rule.detection.named["selection"];
3042 if let Detection::AllOf(items) = det {
3043 if let SigmaValue::String(s) = &items[0].values[0] {
3044 assert_eq!(s.original, "command_and_control");
3045 } else {
3046 panic!("Expected String");
3047 }
3048 } else {
3049 panic!("Expected AllOf");
3050 }
3051 }
3052
3053 #[test]
3054 fn test_case_transformation_snake_case_already_lowercase() {
3055 let mut rule = make_test_rule(); let mut state = PipelineState::default();
3057 let t = Transformation::CaseTransformation {
3058 case_type: "snake_case".to_string(),
3059 };
3060 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3061
3062 let det = &rule.detection.named["selection"];
3063 if let Detection::AllOf(items) = det
3064 && let SigmaValue::String(s) = &items[0].values[0]
3065 {
3066 assert_eq!(s.original, "whoami"); }
3068 }
3069}