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(), 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.get("custom.key"),
2071 Some(&"custom_value".to_string())
2072 );
2073 }
2074
2075 #[test]
2076 fn test_case_transformation_lower() {
2077 let mut rule = make_test_rule();
2078 let mut state = PipelineState::default();
2079 let t = Transformation::CaseTransformation {
2080 case_type: "lower".to_string(),
2081 };
2082 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2083
2084 let det = &rule.detection.named["selection"];
2085 if let Detection::AllOf(items) = det {
2086 if let SigmaValue::String(s) = &items[0].values[0] {
2088 assert_eq!(s.original, "whoami");
2089 }
2090 if let SigmaValue::String(s) = &items[1].values[0] {
2092 assert_eq!(s.original, "\\cmd.exe");
2093 }
2094 }
2095 }
2096
2097 #[test]
2098 fn test_case_transformation_upper() {
2099 let mut rule = make_test_rule();
2100 let mut state = PipelineState::default();
2101 let t = Transformation::CaseTransformation {
2102 case_type: "upper".to_string(),
2103 };
2104 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2105
2106 let det = &rule.detection.named["selection"];
2107 if let Detection::AllOf(items) = det {
2108 if let SigmaValue::String(s) = &items[0].values[0] {
2109 assert_eq!(s.original, "WHOAMI");
2110 }
2111 if let SigmaValue::String(s) = &items[1].values[0] {
2112 assert_eq!(s.original, "\\CMD.EXE");
2113 }
2114 }
2115 }
2116
2117 #[test]
2118 fn test_nest_transformation() {
2119 let mut rule = make_test_rule();
2120 let mut state = PipelineState::default();
2121
2122 let items = vec![
2124 super::super::TransformationItem {
2125 id: Some("inner_prefix".to_string()),
2126 transformation: Transformation::FieldNamePrefix {
2127 prefix: "winlog.".to_string(),
2128 },
2129 rule_conditions: vec![],
2130 rule_cond_expr: None,
2131 detection_item_conditions: vec![],
2132 field_name_conditions: vec![],
2133 field_name_cond_not: false,
2134 },
2135 super::super::TransformationItem {
2136 id: Some("inner_suffix".to_string()),
2137 transformation: Transformation::FieldNameSuffix {
2138 suffix: ".keyword".to_string(),
2139 },
2140 rule_conditions: vec![],
2141 rule_cond_expr: None,
2142 detection_item_conditions: vec![],
2143 field_name_conditions: vec![],
2144 field_name_cond_not: false,
2145 },
2146 ];
2147
2148 let t = Transformation::Nest { items };
2149 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2150
2151 let det = &rule.detection.named["selection"];
2152 if let Detection::AllOf(items) = det {
2153 assert_eq!(
2154 items[0].field.name,
2155 Some("winlog.CommandLine.keyword".to_string())
2156 );
2157 assert_eq!(
2158 items[1].field.name,
2159 Some("winlog.ParentImage.keyword".to_string())
2160 );
2161 } else {
2162 panic!("Expected AllOf");
2163 }
2164
2165 assert!(state.was_applied("inner_prefix"));
2167 assert!(state.was_applied("inner_suffix"));
2168 }
2169
2170 #[test]
2175 fn test_field_name_prefix_mapping() {
2176 let mut rule = make_test_rule();
2177 let mut state = PipelineState::default();
2178 let mut mapping = HashMap::new();
2179 mapping.insert("Command".to_string(), "process.".to_string());
2180 mapping.insert("Parent".to_string(), "process.parent.".to_string());
2181
2182 let t = Transformation::FieldNamePrefixMapping { mapping };
2183 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2184
2185 let det = &rule.detection.named["selection"];
2186 if let Detection::AllOf(items) = det {
2187 assert_eq!(items[0].field.name, Some("process.Line".to_string()));
2189 assert_eq!(
2191 items[1].field.name,
2192 Some("process.parent.Image".to_string())
2193 );
2194 } else {
2195 panic!("Expected AllOf");
2196 }
2197 }
2198
2199 #[test]
2200 fn test_field_name_prefix_mapping_no_match() {
2201 let mut rule = make_test_rule();
2202 let mut state = PipelineState::default();
2203 let mut mapping = HashMap::new();
2204 mapping.insert("NoMatch".to_string(), "replaced.".to_string());
2205
2206 let t = Transformation::FieldNamePrefixMapping { mapping };
2207 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2208
2209 let det = &rule.detection.named["selection"];
2211 if let Detection::AllOf(items) = det {
2212 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2213 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
2214 } else {
2215 panic!("Expected AllOf");
2216 }
2217 }
2218
2219 #[test]
2220 fn test_wildcard_placeholders_replaces_unresolved() {
2221 let mut named = HashMap::new();
2222 named.insert(
2223 "selection".to_string(),
2224 Detection::AllOf(vec![DetectionItem {
2225 field: FieldSpec::new(Some("User".to_string()), vec![]),
2226 values: vec![SigmaValue::String(SigmaString::new("%unknown_var%"))],
2227 }]),
2228 );
2229
2230 let mut rule = SigmaRule {
2231 title: "Test".to_string(),
2232 logsource: LogSource::default(),
2233 detection: Detections {
2234 named,
2235 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2236 condition_strings: vec!["selection".to_string()],
2237 timeframe: None,
2238 },
2239 id: None,
2240 name: None,
2241 related: vec![],
2242 taxonomy: None,
2243 status: None,
2244 description: None,
2245 license: None,
2246 author: None,
2247 references: vec![],
2248 date: None,
2249 modified: None,
2250 fields: vec![],
2251 falsepositives: vec![],
2252 level: None,
2253 tags: vec![],
2254 scope: vec![],
2255 custom_attributes: HashMap::new(),
2256 };
2257
2258 let mut state = PipelineState::default();
2259 let t = Transformation::WildcardPlaceholders;
2261 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2262
2263 let det = &rule.detection.named["selection"];
2264 if let Detection::AllOf(items) = det {
2265 if let SigmaValue::String(s) = &items[0].values[0] {
2266 assert_eq!(s.original, "*", "unresolved placeholder should become *");
2267 } else {
2268 panic!("Expected String value");
2269 }
2270 } else {
2271 panic!("Expected AllOf");
2272 }
2273 }
2274
2275 #[test]
2276 fn test_wildcard_placeholders_with_known_var() {
2277 let mut named = HashMap::new();
2278 named.insert(
2279 "selection".to_string(),
2280 Detection::AllOf(vec![DetectionItem {
2281 field: FieldSpec::new(Some("User".to_string()), vec![]),
2282 values: vec![SigmaValue::String(SigmaString::new("%admin%"))],
2283 }]),
2284 );
2285
2286 let mut rule = SigmaRule {
2287 title: "Test".to_string(),
2288 logsource: LogSource::default(),
2289 detection: Detections {
2290 named,
2291 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2292 condition_strings: vec!["selection".to_string()],
2293 timeframe: None,
2294 },
2295 id: None,
2296 name: None,
2297 related: vec![],
2298 taxonomy: None,
2299 status: None,
2300 description: None,
2301 license: None,
2302 author: None,
2303 references: vec![],
2304 date: None,
2305 modified: None,
2306 fields: vec![],
2307 falsepositives: vec![],
2308 level: None,
2309 tags: vec![],
2310 scope: vec![],
2311 custom_attributes: HashMap::new(),
2312 };
2313
2314 let mut state = PipelineState::default();
2315 state
2316 .vars
2317 .insert("admin".to_string(), vec!["root".to_string()]);
2318
2319 let t = Transformation::WildcardPlaceholders;
2321 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2322
2323 let det = &rule.detection.named["selection"];
2324 if let Detection::AllOf(items) = det {
2325 if let SigmaValue::String(s) = &items[0].values[0] {
2326 assert_eq!(s.original, "root");
2327 } else {
2328 panic!("Expected String value");
2329 }
2330 } else {
2331 panic!("Expected AllOf");
2332 }
2333 }
2334
2335 #[test]
2336 fn test_detection_item_failure_fires_on_match() {
2337 let mut rule = make_test_rule();
2338 let mut state = PipelineState::default();
2339
2340 let det_conds = vec![DetectionItemCondition::MatchString {
2342 regex: regex::Regex::new("whoami").unwrap(),
2343 negate: false,
2344 }];
2345
2346 let t = Transformation::DetectionItemFailure {
2347 message: "Unsupported detection item".to_string(),
2348 };
2349 let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
2350 assert!(result.is_err());
2351 let err = result.unwrap_err().to_string();
2352 assert!(err.contains("Unsupported detection item"));
2353 }
2354
2355 #[test]
2356 fn test_detection_item_failure_skips_on_no_match() {
2357 let mut rule = make_test_rule();
2358 let mut state = PipelineState::default();
2359
2360 let det_conds = vec![DetectionItemCondition::MatchString {
2362 regex: regex::Regex::new("nonexistent_value").unwrap(),
2363 negate: false,
2364 }];
2365
2366 let t = Transformation::DetectionItemFailure {
2367 message: "Should not fire".to_string(),
2368 };
2369 let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
2370 assert!(result.is_ok());
2371 assert!(!result.unwrap()); }
2373
2374 #[test]
2375 fn test_query_expression_placeholders_noop() {
2376 let mut rule = make_test_rule();
2377 let mut state = PipelineState::default();
2378 let t = Transformation::QueryExpressionPlaceholders {
2379 expression: "{field}={value}".to_string(),
2380 };
2381 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2382 assert!(!result); }
2384
2385 #[test]
2390 fn test_add_condition_negated() {
2391 let mut rule = make_test_rule();
2392 let mut state = PipelineState::default();
2393 let mut conds = HashMap::new();
2394 conds.insert(
2395 "User".to_string(),
2396 SigmaValue::String(SigmaString::new("SYSTEM")),
2397 );
2398 let t = Transformation::AddCondition {
2399 conditions: conds,
2400 negated: true,
2401 };
2402 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2403
2404 assert_eq!(rule.detection.conditions.len(), 1);
2406 if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
2407 assert_eq!(parts.len(), 2);
2408 assert!(
2410 matches!(&parts[1], ConditionExpr::Not(_)),
2411 "Expected negated condition, got: {:?}",
2412 parts[1]
2413 );
2414 } else {
2415 panic!("Expected And condition");
2416 }
2417 }
2418
2419 #[test]
2424 fn test_replace_string_with_detection_item_condition() {
2425 let mut rule = make_test_rule();
2426 let mut state = PipelineState::default();
2427
2428 let det_conds = vec![DetectionItemCondition::MatchString {
2430 regex: regex::Regex::new("whoami").unwrap(),
2431 negate: false,
2432 }];
2433
2434 let t = Transformation::ReplaceString {
2435 regex: r"whoami".to_string(),
2436 replacement: "REPLACED".to_string(),
2437 skip_special: false,
2438 };
2439 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2440 .unwrap();
2441
2442 let det = &rule.detection.named["selection"];
2443 if let Detection::AllOf(items) = det {
2444 if let SigmaValue::String(s) = &items[0].values[0] {
2446 assert_eq!(s.original, "REPLACED");
2447 }
2448 if let SigmaValue::String(s) = &items[1].values[0] {
2450 assert_eq!(s.original, "\\cmd.exe");
2451 }
2452 } else {
2453 panic!("Expected AllOf");
2454 }
2455 }
2456
2457 #[test]
2458 fn test_set_value_with_is_null_condition() {
2459 let mut named = HashMap::new();
2461 named.insert(
2462 "selection".to_string(),
2463 Detection::AllOf(vec![
2464 DetectionItem {
2465 field: FieldSpec::new(Some("FieldA".to_string()), vec![]),
2466 values: vec![SigmaValue::Null],
2467 },
2468 DetectionItem {
2469 field: FieldSpec::new(Some("FieldB".to_string()), vec![]),
2470 values: vec![SigmaValue::String(SigmaString::new("value"))],
2471 },
2472 ]),
2473 );
2474
2475 let mut rule = make_test_rule();
2476 rule.detection.named = named;
2477 let mut state = PipelineState::default();
2478
2479 let det_conds = vec![DetectionItemCondition::IsNull { negate: false }];
2481
2482 let t = Transformation::SetValue {
2483 value: SigmaValue::String(SigmaString::new("DEFAULT")),
2484 };
2485 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2486 .unwrap();
2487
2488 let det = &rule.detection.named["selection"];
2489 if let Detection::AllOf(items) = det {
2490 if let SigmaValue::String(s) = &items[0].values[0] {
2492 assert_eq!(s.original, "DEFAULT");
2493 } else {
2494 panic!("Expected String after set_value on null");
2495 }
2496 if let SigmaValue::String(s) = &items[1].values[0] {
2498 assert_eq!(s.original, "value");
2499 }
2500 } else {
2501 panic!("Expected AllOf");
2502 }
2503 }
2504
2505 #[test]
2506 fn test_drop_detection_item_with_match_string_condition() {
2507 let mut rule = make_test_rule();
2508 let mut state = PipelineState::default();
2509
2510 let det_conds = vec![DetectionItemCondition::MatchString {
2512 regex: regex::Regex::new("whoami").unwrap(),
2513 negate: false,
2514 }];
2515
2516 let t = Transformation::DropDetectionItem;
2517 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2518 .unwrap();
2519
2520 let det = &rule.detection.named["selection"];
2521 if let Detection::AllOf(items) = det {
2522 assert_eq!(items.len(), 1);
2523 assert_eq!(items[0].field.name, Some("ParentImage".to_string()));
2525 } else {
2526 panic!("Expected AllOf");
2527 }
2528 }
2529
2530 #[test]
2535 fn test_field_name_mapping_with_cond_not() {
2536 let mut rule = make_test_rule();
2537 let mut state = PipelineState::default();
2538
2539 let field_conds = vec![FieldNameCondition::IncludeFields {
2541 matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
2542 }];
2543
2544 let mut mapping = HashMap::new();
2545 mapping.insert("CommandLine".to_string(), "cmd".to_string());
2546 mapping.insert("ParentImage".to_string(), "parent".to_string());
2547
2548 let t = Transformation::FieldNameMapping { mapping };
2549 t.apply(&mut rule, &mut state, &[], &field_conds, true)
2551 .unwrap();
2552
2553 let det = &rule.detection.named["selection"];
2554 if let Detection::AllOf(items) = det {
2555 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2557 assert_eq!(items[1].field.name, Some("parent".to_string()));
2559 } else {
2560 panic!("Expected AllOf");
2561 }
2562 }
2563
2564 #[test]
2569 fn test_field_name_mapping_empty() {
2570 let mut rule = make_test_rule();
2571 let mut state = PipelineState::default();
2572 let t = Transformation::FieldNameMapping {
2573 mapping: HashMap::new(),
2574 };
2575 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2576
2577 let det = &rule.detection.named["selection"];
2579 if let Detection::AllOf(items) = det {
2580 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2581 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
2582 } else {
2583 panic!("Expected AllOf");
2584 }
2585 }
2586
2587 #[test]
2588 fn test_field_name_prefix_mapping_empty() {
2589 let mut rule = make_test_rule();
2590 let mut state = PipelineState::default();
2591 let t = Transformation::FieldNamePrefixMapping {
2592 mapping: HashMap::new(),
2593 };
2594 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2595
2596 let det = &rule.detection.named["selection"];
2597 if let Detection::AllOf(items) = det {
2598 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2599 } else {
2600 panic!("Expected AllOf");
2601 }
2602 }
2603
2604 #[test]
2605 fn test_map_string_empty_mapping() {
2606 let mut rule = make_test_rule();
2607 let mut state = PipelineState::default();
2608 let t = Transformation::MapString {
2609 mapping: HashMap::new(),
2610 };
2611 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2612
2613 let det = &rule.detection.named["selection"];
2614 if let Detection::AllOf(items) = det
2615 && let SigmaValue::String(s) = &items[0].values[0]
2616 {
2617 assert_eq!(s.original, "whoami");
2618 }
2619 }
2620
2621 #[test]
2622 fn test_hashes_fields_empty_algos() {
2623 let mut named = HashMap::new();
2625 named.insert(
2626 "selection".to_string(),
2627 Detection::AllOf(vec![DetectionItem {
2628 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2629 values: vec![SigmaValue::String(SigmaString::new(
2630 "SHA256=abc123,IMPHASH=def456",
2631 ))],
2632 }]),
2633 );
2634
2635 let mut rule = make_test_rule();
2636 rule.detection.named = named;
2637
2638 let mut state = PipelineState::default();
2639 let t = Transformation::HashesFields {
2640 valid_hash_algos: vec![], field_prefix: "File".to_string(),
2642 drop_algo_prefix: false,
2643 };
2644 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2645
2646 let det = &rule.detection.named["selection"];
2647 if let Detection::AllOf(items) = det {
2648 assert_eq!(items.len(), 2);
2649 assert_eq!(items[0].field.name, Some("FileSHA256".to_string()));
2650 assert_eq!(items[1].field.name, Some("FileIMPHASH".to_string()));
2651 } else {
2652 panic!("Expected AllOf");
2653 }
2654 }
2655
2656 #[test]
2657 fn test_hashes_fields_drop_algo_prefix() {
2658 let mut named = HashMap::new();
2659 named.insert(
2660 "selection".to_string(),
2661 Detection::AllOf(vec![DetectionItem {
2662 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2663 values: vec![SigmaValue::String(SigmaString::new("MD5=abc123"))],
2664 }]),
2665 );
2666
2667 let mut rule = make_test_rule();
2668 rule.detection.named = named;
2669 let mut state = PipelineState::default();
2670
2671 let t = Transformation::HashesFields {
2672 valid_hash_algos: vec!["MD5".to_string()],
2673 field_prefix: "Hash".to_string(),
2674 drop_algo_prefix: true,
2675 };
2676 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2677
2678 let det = &rule.detection.named["selection"];
2679 if let Detection::AllOf(items) = det {
2680 assert_eq!(items.len(), 1);
2681 assert_eq!(items[0].field.name, Some("Hash".to_string()));
2683 } else {
2684 panic!("Expected AllOf");
2685 }
2686 }
2687
2688 #[test]
2693 fn test_replace_string_invalid_regex() {
2694 let mut rule = make_test_rule();
2695 let mut state = PipelineState::default();
2696 let t = Transformation::ReplaceString {
2697 regex: r"[invalid".to_string(), replacement: "x".to_string(),
2699 skip_special: false,
2700 };
2701 let result = t.apply(&mut rule, &mut state, &[], &[], false);
2702 assert!(result.is_err());
2703 let err = result.unwrap_err().to_string();
2704 assert!(
2705 err.contains("bad regex"),
2706 "error should mention regex: {err}"
2707 );
2708 }
2709
2710 #[test]
2715 fn test_case_transformation_with_negated_match_string() {
2716 let mut rule = make_test_rule();
2717 let mut state = PipelineState::default();
2718
2719 let det_conds = vec![DetectionItemCondition::MatchString {
2721 regex: regex::Regex::new("whoami").unwrap(),
2722 negate: true,
2723 }];
2724
2725 let t = Transformation::CaseTransformation {
2726 case_type: "upper".to_string(),
2727 };
2728 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2729 .unwrap();
2730
2731 let det = &rule.detection.named["selection"];
2732 if let Detection::AllOf(items) = det {
2733 if let SigmaValue::String(s) = &items[0].values[0] {
2735 assert_eq!(s.original, "whoami");
2736 }
2737 if let SigmaValue::String(s) = &items[1].values[0] {
2739 assert_eq!(s.original, "\\CMD.EXE");
2740 }
2741 } else {
2742 panic!("Expected AllOf");
2743 }
2744 }
2745
2746 #[test]
2751 fn test_multi_transformation_chaining_pipeline() {
2752 use crate::pipeline::parse_pipeline;
2753
2754 let yaml = r#"
2755name: Multi-step Pipeline
2756transformations:
2757 - id: step1_map
2758 type: field_name_mapping
2759 mapping:
2760 CommandLine: process.command_line
2761 ParentImage: process.parent.executable
2762 - id: step2_prefix
2763 type: field_name_prefix
2764 prefix: "winlog."
2765 rule_conditions:
2766 - type: logsource
2767 product: windows
2768 - id: step3_case
2769 type: case_transformation
2770 case_type: upper
2771 field_name_conditions:
2772 - type: include_fields
2773 fields:
2774 - winlog.process.command_line
2775 - id: step4_attr
2776 type: set_custom_attribute
2777 attribute: rsigma.processed
2778 value: "true"
2779"#;
2780 let pipeline = parse_pipeline(yaml).unwrap();
2781
2782 let mut rule = make_test_rule(); let mut state = PipelineState::new(pipeline.vars.clone());
2784 pipeline.apply(&mut rule, &mut state).unwrap();
2785
2786 let det = &rule.detection.named["selection"];
2787 if let Detection::AllOf(items) = det {
2788 assert_eq!(
2791 items[0].field.name,
2792 Some("winlog.process.command_line".to_string())
2793 );
2794 if let SigmaValue::String(s) = &items[0].values[0] {
2796 assert_eq!(s.original, "WHOAMI");
2797 }
2798
2799 assert_eq!(
2801 items[1].field.name,
2802 Some("winlog.process.parent.executable".to_string())
2803 );
2804 if let SigmaValue::String(s) = &items[1].values[0] {
2806 assert_eq!(s.original, "\\cmd.exe");
2807 }
2808 } else {
2809 panic!("Expected AllOf");
2810 }
2811
2812 assert_eq!(
2814 rule.custom_attributes.get("rsigma.processed"),
2815 Some(&"true".to_string())
2816 );
2817
2818 assert!(state.was_applied("step1_map"));
2820 assert!(state.was_applied("step2_prefix"));
2821 assert!(state.was_applied("step3_case"));
2822 assert!(state.was_applied("step4_attr"));
2823 }
2824
2825 #[test]
2830 fn test_map_string_one_to_many() {
2831 let mut rule = make_test_rule();
2832 let mut state = PipelineState::default();
2833 let mut mapping = HashMap::new();
2834 mapping.insert(
2835 "whoami".to_string(),
2836 vec!["who".to_string(), "am_i".to_string(), "test".to_string()],
2837 );
2838 let t = Transformation::MapString { mapping };
2839 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2840
2841 let det = &rule.detection.named["selection"];
2842 if let Detection::AllOf(items) = det {
2843 assert_eq!(items[0].values.len(), 3);
2845 if let SigmaValue::String(s) = &items[0].values[0] {
2846 assert_eq!(s.original, "who");
2847 }
2848 if let SigmaValue::String(s) = &items[0].values[1] {
2849 assert_eq!(s.original, "am_i");
2850 }
2851 if let SigmaValue::String(s) = &items[0].values[2] {
2852 assert_eq!(s.original, "test");
2853 }
2854 } else {
2855 panic!("Expected AllOf");
2856 }
2857 }
2858
2859 #[test]
2860 fn test_map_string_one_to_many_mixed() {
2861 let yaml = r#"
2863title: Test Rule
2864logsource:
2865 product: windows
2866detection:
2867 selection:
2868 CommandLine:
2869 - whoami
2870 - ipconfig
2871 condition: selection
2872level: medium
2873"#;
2874 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
2875 let mut rule = collection.rules[0].clone();
2876 let mut state = PipelineState::default();
2877 let mut mapping = HashMap::new();
2878 mapping.insert(
2879 "whoami".to_string(),
2880 vec!["who".to_string(), "am_i".to_string()],
2881 );
2882 let t = Transformation::MapString { mapping };
2884 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2885
2886 let det = &rule.detection.named["selection"];
2887 if let Detection::AllOf(items) = det {
2888 assert_eq!(items[0].values.len(), 3);
2890 if let SigmaValue::String(s) = &items[0].values[0] {
2891 assert_eq!(s.original, "who");
2892 }
2893 if let SigmaValue::String(s) = &items[0].values[1] {
2894 assert_eq!(s.original, "am_i");
2895 }
2896 if let SigmaValue::String(s) = &items[0].values[2] {
2897 assert_eq!(s.original, "ipconfig");
2898 }
2899 } else {
2900 panic!("Expected AllOf");
2901 }
2902 }
2903
2904 #[test]
2909 fn test_replace_string_skip_special_preserves_wildcards() {
2910 let yaml = r#"
2912title: Test Rule
2913logsource:
2914 product: windows
2915detection:
2916 selection:
2917 CommandLine: '*whoami*'
2918 condition: selection
2919level: medium
2920"#;
2921 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
2922 let mut rule = collection.rules[0].clone();
2923 let mut state = PipelineState::default();
2924 let t = Transformation::ReplaceString {
2925 regex: r"whoami".to_string(),
2926 replacement: "REPLACED".to_string(),
2927 skip_special: true,
2928 };
2929 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2930
2931 let det = &rule.detection.named["selection"];
2932 if let Detection::AllOf(items) = det {
2933 let s = match &items[0].values[0] {
2934 SigmaValue::String(s) => s,
2935 _ => panic!("Expected String"),
2936 };
2937 assert!(s.contains_wildcards(), "Wildcards should be preserved");
2939 assert!(
2940 s.original.contains("REPLACED"),
2941 "Plain part should be replaced, got: {}",
2942 s.original
2943 );
2944 assert!(
2945 !s.original.contains("whoami"),
2946 "Original text should be gone"
2947 );
2948 } else {
2949 panic!("Expected AllOf");
2950 }
2951 }
2952
2953 #[test]
2954 fn test_replace_string_skip_special_false_replaces_whole() {
2955 let yaml = r#"
2957title: Test Rule
2958logsource:
2959 product: windows
2960detection:
2961 selection:
2962 CommandLine: '*whoami*'
2963 condition: selection
2964level: medium
2965"#;
2966 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
2967 let mut rule = collection.rules[0].clone();
2968 let mut state = PipelineState::default();
2969 let t = Transformation::ReplaceString {
2970 regex: r"\*".to_string(),
2971 replacement: "STAR".to_string(),
2972 skip_special: false,
2973 };
2974 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2975
2976 let det = &rule.detection.named["selection"];
2977 if let Detection::AllOf(items) = det {
2978 let s = match &items[0].values[0] {
2979 SigmaValue::String(s) => s,
2980 _ => panic!("Expected String"),
2981 };
2982 assert!(
2984 s.original.contains("STAR"),
2985 "Wildcards in original should be replaced as text, got: {}",
2986 s.original
2987 );
2988 } else {
2989 panic!("Expected AllOf");
2990 }
2991 }
2992
2993 #[test]
2994 fn test_replace_string_skip_special_plain_string() {
2995 let mut rule = make_test_rule();
2997 let mut state = PipelineState::default();
2998 let t = Transformation::ReplaceString {
2999 regex: r"whoami".to_string(),
3000 replacement: "REPLACED".to_string(),
3001 skip_special: true,
3002 };
3003 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3004
3005 let det = &rule.detection.named["selection"];
3006 if let Detection::AllOf(items) = det
3007 && let SigmaValue::String(s) = &items[0].values[0]
3008 {
3009 assert_eq!(s.original, "REPLACED");
3010 }
3011 }
3012
3013 #[test]
3018 fn test_case_transformation_snake_case() {
3019 let yaml = r#"
3020title: Test Rule
3021logsource:
3022 product: windows
3023detection:
3024 selection:
3025 CommandLine: CommandAndControl
3026 condition: selection
3027level: medium
3028"#;
3029 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
3030 let mut rule = collection.rules[0].clone();
3031 let mut state = PipelineState::default();
3032 let t = Transformation::CaseTransformation {
3033 case_type: "snake_case".to_string(),
3034 };
3035 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3036
3037 let det = &rule.detection.named["selection"];
3038 if let Detection::AllOf(items) = det {
3039 if let SigmaValue::String(s) = &items[0].values[0] {
3040 assert_eq!(s.original, "command_and_control");
3041 } else {
3042 panic!("Expected String");
3043 }
3044 } else {
3045 panic!("Expected AllOf");
3046 }
3047 }
3048
3049 #[test]
3050 fn test_case_transformation_snake_case_already_lowercase() {
3051 let mut rule = make_test_rule(); let mut state = PipelineState::default();
3053 let t = Transformation::CaseTransformation {
3054 case_type: "snake_case".to_string(),
3055 };
3056 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3057
3058 let det = &rule.detection.named["selection"];
3059 if let Detection::AllOf(items) = det
3060 && let SigmaValue::String(s) = &items[0].values[0]
3061 {
3062 assert_eq!(s.original, "whoami"); }
3064 }
3065}