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 {
41 mapping: HashMap<String, Vec<String>>,
42 },
43
44 FieldNamePrefixMapping { mapping: HashMap<String, String> },
46
47 FieldNamePrefix { prefix: String },
49
50 FieldNameSuffix { suffix: String },
52
53 DropDetectionItem,
55
56 AddCondition {
58 conditions: HashMap<String, SigmaValue>,
59 negated: bool,
61 },
62
63 ChangeLogsource {
65 category: Option<String>,
66 product: Option<String>,
67 service: Option<String>,
68 },
69
70 ReplaceString {
76 regex: String,
77 replacement: String,
78 skip_special: bool,
79 },
80
81 ValuePlaceholders,
83
84 WildcardPlaceholders,
86
87 QueryExpressionPlaceholders { expression: String },
89
90 SetState { key: String, value: String },
92
93 RuleFailure { message: String },
95
96 DetectionItemFailure { message: String },
98
99 FieldNameTransform {
102 transform_func: String,
104 mapping: HashMap<String, String>,
106 },
107
108 HashesFields {
112 valid_hash_algos: Vec<String>,
114 field_prefix: String,
116 drop_algo_prefix: bool,
118 },
119
120 MapString {
126 mapping: HashMap<String, Vec<String>>,
127 },
128
129 SetValue { value: SigmaValue },
131
132 ConvertType { target_type: String },
135
136 Regex,
138
139 AddField { field: String },
141
142 RemoveField { field: String },
144
145 SetField { fields: Vec<String> },
147
148 SetCustomAttribute { attribute: String, value: String },
155
156 CaseTransformation { case_type: String },
159
160 Nest {
163 items: Vec<super::TransformationItem>,
164 },
165}
166
167impl Transformation {
172 pub fn apply(
176 &self,
177 rule: &mut SigmaRule,
178 state: &mut PipelineState,
179 detection_item_conditions: &[DetectionItemCondition],
180 field_name_conditions: &[FieldNameCondition],
181 field_name_cond_not: bool,
182 ) -> Result<bool> {
183 match self {
184 Transformation::FieldNameMapping { mapping } => {
185 apply_field_name_transform(
186 rule,
187 state,
188 field_name_conditions,
189 field_name_cond_not,
190 |name| mapping.get(name).cloned(),
191 )?;
192 Ok(true)
193 }
194
195 Transformation::FieldNamePrefixMapping { mapping } => {
196 apply_field_name_transform(
197 rule,
198 state,
199 field_name_conditions,
200 field_name_cond_not,
201 |name| {
202 for (prefix, replacement) in mapping {
203 if name.starts_with(prefix.as_str()) {
204 return Some(vec![format!(
205 "{}{}",
206 replacement,
207 &name[prefix.len()..]
208 )]);
209 }
210 }
211 None
212 },
213 )?;
214 Ok(true)
215 }
216
217 Transformation::FieldNamePrefix { prefix } => {
218 apply_field_name_transform(
219 rule,
220 state,
221 field_name_conditions,
222 field_name_cond_not,
223 |name| Some(vec![format!("{prefix}{name}")]),
224 )?;
225 Ok(true)
226 }
227
228 Transformation::FieldNameSuffix { suffix } => {
229 apply_field_name_transform(
230 rule,
231 state,
232 field_name_conditions,
233 field_name_cond_not,
234 |name| Some(vec![format!("{name}{suffix}")]),
235 )?;
236 Ok(true)
237 }
238
239 Transformation::DropDetectionItem => {
240 drop_detection_items(
241 rule,
242 state,
243 detection_item_conditions,
244 field_name_conditions,
245 field_name_cond_not,
246 );
247 Ok(true)
248 }
249
250 Transformation::AddCondition {
251 conditions,
252 negated,
253 } => {
254 add_conditions(rule, conditions, *negated);
255 Ok(true)
256 }
257
258 Transformation::ChangeLogsource {
259 category,
260 product,
261 service,
262 } => {
263 if let Some(cat) = category {
264 rule.logsource.category = Some(cat.clone());
265 }
266 if let Some(prod) = product {
267 rule.logsource.product = Some(prod.clone());
268 }
269 if let Some(svc) = service {
270 rule.logsource.service = Some(svc.clone());
271 }
272 Ok(true)
273 }
274
275 Transformation::ReplaceString {
276 regex,
277 replacement,
278 skip_special,
279 } => {
280 let re = Regex::new(regex)
281 .map_err(|e| EvalError::InvalidModifiers(format!("bad regex: {e}")))?;
282 replace_strings_in_rule(
283 rule,
284 state,
285 detection_item_conditions,
286 field_name_conditions,
287 field_name_cond_not,
288 &re,
289 replacement,
290 *skip_special,
291 );
292 Ok(true)
293 }
294
295 Transformation::ValuePlaceholders => {
296 expand_placeholders_in_rule(rule, state, false);
297 Ok(true)
298 }
299
300 Transformation::WildcardPlaceholders => {
301 expand_placeholders_in_rule(rule, state, true);
302 Ok(true)
303 }
304
305 Transformation::QueryExpressionPlaceholders { expression } => {
306 state.set_state(
307 "query_expression_template".to_string(),
308 serde_json::Value::String(expression.clone()),
309 );
310 Ok(true)
311 }
312
313 Transformation::SetState { key, value } => {
314 state.set_state(key.clone(), serde_json::Value::String(value.clone()));
315 Ok(true)
316 }
317
318 Transformation::RuleFailure { message } => Err(EvalError::InvalidModifiers(format!(
319 "Pipeline rule failure: {message} (rule: {})",
320 rule.title
321 ))),
322
323 Transformation::DetectionItemFailure { message } => {
324 let has_match = rule_has_matching_item(rule, state, detection_item_conditions);
326 if has_match {
327 Err(EvalError::InvalidModifiers(format!(
328 "Pipeline detection item failure: {message} (rule: {})",
329 rule.title
330 )))
331 } else {
332 Ok(false)
333 }
334 }
335
336 Transformation::FieldNameTransform {
337 transform_func,
338 mapping,
339 } => {
340 let func = transform_func.clone();
341 let map = mapping.clone();
342 apply_field_name_transform(
343 rule,
344 state,
345 field_name_conditions,
346 field_name_cond_not,
347 |name| {
348 if let Some(mapped) = map.get(name) {
349 return Some(vec![mapped.clone()]);
350 }
351 Some(vec![apply_named_string_fn(&func, name)])
352 },
353 )?;
354 Ok(true)
355 }
356
357 Transformation::HashesFields {
358 valid_hash_algos,
359 field_prefix,
360 drop_algo_prefix,
361 } => {
362 decompose_hashes_field(rule, valid_hash_algos, field_prefix, *drop_algo_prefix);
363 Ok(true)
364 }
365
366 Transformation::MapString { mapping } => {
367 map_string_values(
368 rule,
369 state,
370 detection_item_conditions,
371 field_name_conditions,
372 field_name_cond_not,
373 mapping,
374 );
375 Ok(true)
376 }
377
378 Transformation::SetValue { value } => {
379 set_detection_item_values(
380 rule,
381 state,
382 detection_item_conditions,
383 field_name_conditions,
384 field_name_cond_not,
385 value,
386 );
387 Ok(true)
388 }
389
390 Transformation::ConvertType { target_type } => {
391 convert_detection_item_types(
392 rule,
393 state,
394 detection_item_conditions,
395 field_name_conditions,
396 field_name_cond_not,
397 target_type,
398 );
399 Ok(true)
400 }
401
402 Transformation::Regex => {
403 Ok(false)
407 }
408
409 Transformation::AddField { field } => {
410 if !rule.fields.contains(field) {
411 rule.fields.push(field.clone());
412 }
413 Ok(true)
414 }
415
416 Transformation::RemoveField { field } => {
417 rule.fields.retain(|f| f != field);
418 Ok(true)
419 }
420
421 Transformation::SetField { fields } => {
422 rule.fields = fields.clone();
423 Ok(true)
424 }
425
426 Transformation::SetCustomAttribute { attribute, value } => {
427 rule.custom_attributes
428 .insert(attribute.clone(), serde_yaml::Value::String(value.clone()));
429 Ok(true)
430 }
431
432 Transformation::CaseTransformation { case_type } => {
433 apply_case_transformation(
434 rule,
435 state,
436 detection_item_conditions,
437 field_name_conditions,
438 field_name_cond_not,
439 case_type,
440 );
441 Ok(true)
442 }
443
444 Transformation::Nest { items } => {
445 for item in items {
446 let mut merged_det_conds: Vec<DetectionItemCondition> =
448 detection_item_conditions.to_vec();
449 merged_det_conds.extend(item.detection_item_conditions.clone());
450
451 let mut merged_field_conds: Vec<FieldNameCondition> =
452 field_name_conditions.to_vec();
453 merged_field_conds.extend(item.field_name_conditions.clone());
454
455 let rule_ok = if item.rule_conditions.is_empty() {
457 true
458 } else {
459 super::conditions::all_rule_conditions_match(
460 &item.rule_conditions,
461 rule,
462 state,
463 )
464 };
465
466 if rule_ok {
467 item.transformation.apply(
468 rule,
469 state,
470 &merged_det_conds,
471 &merged_field_conds,
472 item.field_name_cond_not || field_name_cond_not,
473 )?;
474 if let Some(ref id) = item.id {
475 state.mark_applied(id);
476 }
477 }
478 }
479 Ok(true)
480 }
481 }
482 }
483}
484
485const MAX_FIELD_MAPPING_COMBINATIONS: usize = 4096;
499
500fn apply_field_name_transform<F>(
508 rule: &mut SigmaRule,
509 state: &PipelineState,
510 field_name_conditions: &[FieldNameCondition],
511 field_name_cond_not: bool,
512 transform_fn: F,
513) -> Result<()>
514where
515 F: Fn(&str) -> Option<Vec<String>>,
516{
517 let rule_title = rule.title.clone();
518 for detection in rule.detection.named.values_mut() {
519 transform_detection_fields(
520 detection,
521 state,
522 field_name_conditions,
523 field_name_cond_not,
524 &transform_fn,
525 &rule_title,
526 )?;
527 }
528 Ok(())
529}
530
531fn transform_detection_fields<F>(
532 detection: &mut Detection,
533 state: &PipelineState,
534 field_name_conditions: &[FieldNameCondition],
535 field_name_cond_not: bool,
536 transform_fn: &F,
537 rule_title: &str,
538) -> Result<()>
539where
540 F: Fn(&str) -> Option<Vec<String>>,
541{
542 match detection {
543 Detection::AllOf(items) => {
544 enum Resolved {
547 Unchanged,
548 Renamed(String),
549 Expanded(Vec<String>),
550 }
551 let resolved: Vec<Resolved> = items
552 .iter()
553 .map(|item| match item.field.name.as_deref() {
554 Some(name)
555 if field_conditions_match(
556 name,
557 state,
558 field_name_conditions,
559 field_name_cond_not,
560 ) =>
561 {
562 match transform_fn(name) {
563 Some(new_names) if new_names.len() > 1 => Resolved::Expanded(new_names),
564 Some(mut new_names) if new_names.len() == 1 => {
565 Resolved::Renamed(new_names.pop().unwrap())
566 }
567 _ => Resolved::Unchanged,
568 }
569 }
570 _ => Resolved::Unchanged,
571 })
572 .collect();
573
574 let needs_expansion = resolved.iter().any(|r| matches!(r, Resolved::Expanded(_)));
575
576 if !needs_expansion {
577 for (item, res) in items.iter_mut().zip(resolved) {
579 if let Resolved::Renamed(new_name) = res {
580 item.field.name = Some(new_name);
581 }
582 }
583 } else {
584 let alternatives: Vec<Vec<DetectionItem>> = items
586 .iter()
587 .zip(resolved)
588 .map(|(item, res)| match res {
589 Resolved::Expanded(names) => names
590 .into_iter()
591 .map(|new_name| {
592 let mut clone = item.clone();
593 clone.field.name = Some(new_name);
594 clone
595 })
596 .collect(),
597 Resolved::Renamed(name) => {
598 let mut clone = item.clone();
599 clone.field.name = Some(name);
600 vec![clone]
601 }
602 Resolved::Unchanged => vec![item.clone()],
603 })
604 .collect();
605
606 let total = alternatives
607 .iter()
608 .map(Vec::len)
609 .fold(1usize, |acc, n| acc.saturating_mul(n));
610 if total > MAX_FIELD_MAPPING_COMBINATIONS {
611 let sizes: Vec<usize> = alternatives.iter().map(Vec::len).collect();
612 return Err(EvalError::InvalidModifiers(format!(
613 "field name mapping cartesian expansion would produce {total} \
614 branches, exceeding the limit of {MAX_FIELD_MAPPING_COMBINATIONS} \
615 (rule: {rule_title}, per-item alternative counts: {sizes:?}); \
616 reduce the number of one-to-many alternatives or split the AllOf"
617 )));
618 }
619 let combinations = cartesian_product(alternatives);
620 *detection =
621 Detection::AnyOf(combinations.into_iter().map(Detection::AllOf).collect());
622 }
623 }
624 Detection::AnyOf(subs) => {
625 for sub in subs.iter_mut() {
626 transform_detection_fields(
627 sub,
628 state,
629 field_name_conditions,
630 field_name_cond_not,
631 transform_fn,
632 rule_title,
633 )?;
634 }
635 }
636 Detection::Keywords(_) => {}
637 }
638 Ok(())
639}
640
641fn cartesian_product<T: Clone>(input: Vec<Vec<T>>) -> Vec<Vec<T>> {
647 let mut result: Vec<Vec<T>> = vec![Vec::new()];
648 for group in input {
649 let mut next = Vec::with_capacity(result.len() * group.len().max(1));
650 for prefix in &result {
651 for elem in &group {
652 let mut combo = prefix.clone();
653 combo.push(elem.clone());
654 next.push(combo);
655 }
656 }
657 result = next;
658 }
659 result
660}
661
662fn field_conditions_match(
663 field_name: &str,
664 state: &PipelineState,
665 conditions: &[FieldNameCondition],
666 negate: bool,
667) -> bool {
668 if conditions.is_empty() {
669 return true;
670 }
671 let all_match = conditions
672 .iter()
673 .all(|c| c.matches_field_name(field_name, state));
674 if negate { !all_match } else { all_match }
675}
676
677fn drop_detection_items(
682 rule: &mut SigmaRule,
683 state: &PipelineState,
684 detection_conditions: &[DetectionItemCondition],
685 field_name_conditions: &[FieldNameCondition],
686 field_name_cond_not: bool,
687) {
688 for detection in rule.detection.named.values_mut() {
689 drop_from_detection(
690 detection,
691 state,
692 detection_conditions,
693 field_name_conditions,
694 field_name_cond_not,
695 );
696 }
697}
698
699fn drop_from_detection(
700 detection: &mut Detection,
701 state: &PipelineState,
702 detection_conditions: &[DetectionItemCondition],
703 field_name_conditions: &[FieldNameCondition],
704 field_name_cond_not: bool,
705) {
706 match detection {
707 Detection::AllOf(items) => {
708 items.retain(|item| {
709 !should_drop_item(
710 item,
711 state,
712 detection_conditions,
713 field_name_conditions,
714 field_name_cond_not,
715 )
716 });
717 }
718 Detection::AnyOf(subs) => {
719 for sub in subs.iter_mut() {
720 drop_from_detection(
721 sub,
722 state,
723 detection_conditions,
724 field_name_conditions,
725 field_name_cond_not,
726 );
727 }
728 }
729 Detection::Keywords(_) => {}
730 }
731}
732
733fn should_drop_item(
734 item: &DetectionItem,
735 state: &PipelineState,
736 detection_conditions: &[DetectionItemCondition],
737 field_name_conditions: &[FieldNameCondition],
738 field_name_cond_not: bool,
739) -> bool {
740 let det_match = detection_conditions.is_empty()
742 || detection_conditions
743 .iter()
744 .all(|c| c.matches_item(item, state));
745
746 let field_match = if let Some(ref name) = item.field.name {
748 field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
749 } else {
750 field_name_conditions.is_empty()
751 };
752
753 det_match && field_match
754}
755
756fn add_conditions(rule: &mut SigmaRule, conditions: &HashMap<String, SigmaValue>, negated: bool) {
761 let items: Vec<DetectionItem> = conditions
763 .iter()
764 .map(|(field, value)| DetectionItem {
765 field: FieldSpec::new(Some(field.clone()), Vec::new()),
766 values: vec![value.clone()],
767 })
768 .collect();
769
770 let det_name = format!("__pipeline_cond_{}", rule.detection.named.len());
771 rule.detection
772 .named
773 .insert(det_name.clone(), Detection::AllOf(items));
774
775 let cond_ref = ConditionExpr::Identifier(det_name);
777 let cond_expr = if negated {
778 ConditionExpr::Not(Box::new(cond_ref))
779 } else {
780 cond_ref
781 };
782
783 rule.detection.conditions = rule
784 .detection
785 .conditions
786 .iter()
787 .map(|existing| ConditionExpr::And(vec![existing.clone(), cond_expr.clone()]))
788 .collect();
789}
790
791#[allow(clippy::too_many_arguments)]
796fn replace_strings_in_rule(
797 rule: &mut SigmaRule,
798 state: &PipelineState,
799 detection_conditions: &[DetectionItemCondition],
800 field_name_conditions: &[FieldNameCondition],
801 field_name_cond_not: bool,
802 re: &Regex,
803 replacement: &str,
804 skip_special: bool,
805) {
806 for detection in rule.detection.named.values_mut() {
807 replace_strings_in_detection(
808 detection,
809 state,
810 detection_conditions,
811 field_name_conditions,
812 field_name_cond_not,
813 re,
814 replacement,
815 skip_special,
816 );
817 }
818}
819
820#[allow(clippy::too_many_arguments)]
821fn replace_strings_in_detection(
822 detection: &mut Detection,
823 state: &PipelineState,
824 detection_conditions: &[DetectionItemCondition],
825 field_name_conditions: &[FieldNameCondition],
826 field_name_cond_not: bool,
827 re: &Regex,
828 replacement: &str,
829 skip_special: bool,
830) {
831 match detection {
832 Detection::AllOf(items) => {
833 for item in items.iter_mut() {
834 let det_match = detection_conditions.is_empty()
835 || detection_conditions
836 .iter()
837 .all(|c| c.matches_item(item, state));
838 let field_match = if let Some(ref name) = item.field.name {
839 field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
840 } else {
841 field_name_conditions.is_empty()
842 };
843
844 if det_match && field_match {
845 replace_strings_in_values(&mut item.values, re, replacement, skip_special);
846 }
847 }
848 }
849 Detection::AnyOf(subs) => {
850 for sub in subs.iter_mut() {
851 replace_strings_in_detection(
852 sub,
853 state,
854 detection_conditions,
855 field_name_conditions,
856 field_name_cond_not,
857 re,
858 replacement,
859 skip_special,
860 );
861 }
862 }
863 Detection::Keywords(values) => {
864 replace_strings_in_values(values, re, replacement, skip_special);
865 }
866 }
867}
868
869fn replace_strings_in_values(
870 values: &mut [SigmaValue],
871 re: &Regex,
872 replacement: &str,
873 skip_special: bool,
874) {
875 for value in values.iter_mut() {
876 if let SigmaValue::String(s) = value {
877 if skip_special && s.contains_wildcards() {
878 let new_parts: Vec<StringPart> = s
880 .parts
881 .iter()
882 .map(|part| match part {
883 StringPart::Plain(text) => {
884 let replaced = re.replace_all(text, replacement);
885 StringPart::Plain(replaced.into_owned())
886 }
887 special => special.clone(),
888 })
889 .collect();
890 if new_parts != s.parts {
891 let new_original = parts_to_original(&new_parts);
893 s.parts = new_parts;
894 s.original = new_original;
895 }
896 } else {
897 let replaced = re.replace_all(&s.original, replacement);
898 if replaced != s.original {
899 *s = SigmaString::new(&replaced);
900 }
901 }
902 }
903 }
904}
905
906fn parts_to_original(parts: &[StringPart]) -> String {
908 let mut out = String::new();
909 for part in parts {
910 match part {
911 StringPart::Plain(text) => {
912 for c in text.chars() {
914 if c == '*' || c == '?' || c == '\\' {
915 out.push('\\');
916 }
917 out.push(c);
918 }
919 }
920 StringPart::Special(SpecialChar::WildcardMulti) => out.push('*'),
921 StringPart::Special(SpecialChar::WildcardSingle) => out.push('?'),
922 }
923 }
924 out
925}
926
927fn expand_placeholders_in_rule(rule: &mut SigmaRule, state: &PipelineState, wildcard: bool) {
932 for detection in rule.detection.named.values_mut() {
933 expand_placeholders_in_detection(detection, state, wildcard);
934 }
935}
936
937fn expand_placeholders_in_detection(
938 detection: &mut Detection,
939 state: &PipelineState,
940 wildcard: bool,
941) {
942 match detection {
943 Detection::AllOf(items) => {
944 for item in items.iter_mut() {
945 expand_placeholders_in_values(&mut item.values, state, wildcard);
946 }
947 }
948 Detection::AnyOf(subs) => {
949 for sub in subs.iter_mut() {
950 expand_placeholders_in_detection(sub, state, wildcard);
951 }
952 }
953 Detection::Keywords(values) => {
954 expand_placeholders_in_values(values, state, wildcard);
955 }
956 }
957}
958
959fn expand_placeholders_in_values(
960 values: &mut Vec<SigmaValue>,
961 state: &PipelineState,
962 wildcard: bool,
963) {
964 let mut expanded_values = Vec::new();
965 for value in values.drain(..) {
966 if let SigmaValue::String(ref s) = value {
967 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
968 if plain.contains('%') {
969 let result = expand_placeholder_string(&plain, state, wildcard);
971 expanded_values.extend(result);
972 continue;
973 }
974 }
975 expanded_values.push(value);
976 }
977 *values = expanded_values;
978}
979
980fn expand_placeholder_string(s: &str, state: &PipelineState, wildcard: bool) -> Vec<SigmaValue> {
981 let mut result = s.to_string();
983 let mut has_unresolved = false;
984
985 while let Some(start) = result.find('%') {
987 let rest = &result[start + 1..];
988 let Some(end) = rest.find('%') else {
989 break;
990 };
991 let placeholder = &rest[..end];
992
993 if let Some(values) = state.vars.get(placeholder) {
994 if values.len() == 1 {
995 result = format!("{}{}{}", &result[..start], values[0], &rest[end + 1..]);
996 } else if values.is_empty() {
997 if wildcard {
998 result = format!("{}*{}", &result[..start], &rest[end + 1..]);
999 } else {
1000 has_unresolved = true;
1001 break;
1002 }
1003 } else {
1004 return values
1006 .iter()
1007 .map(|v| {
1008 let expanded = format!("{}{}{}", &result[..start], v, &rest[end + 1..]);
1009 SigmaValue::String(SigmaString::new(&expanded))
1010 })
1011 .collect();
1012 }
1013 } else if wildcard {
1014 result = format!("{}*{}", &result[..start], &rest[end + 1..]);
1015 } else {
1016 has_unresolved = true;
1017 break;
1018 }
1019 }
1020
1021 if has_unresolved && wildcard {
1022 vec![SigmaValue::String(SigmaString::new(&result))]
1025 } else {
1026 vec![SigmaValue::String(SigmaString::new(&result))]
1027 }
1028}
1029
1030fn apply_named_string_fn(func: &str, s: &str) -> String {
1035 match func {
1036 "lower" | "lowercase" => s.to_lowercase(),
1037 "upper" | "uppercase" => s.to_uppercase(),
1038 "title" => {
1039 s.split(|c: char| !c.is_alphanumeric())
1041 .filter(|w| !w.is_empty())
1042 .map(|w| {
1043 let mut c = w.chars();
1044 match c.next() {
1045 None => String::new(),
1046 Some(f) => {
1047 f.to_uppercase().collect::<String>() + &c.as_str().to_lowercase()
1048 }
1049 }
1050 })
1051 .collect::<Vec<_>>()
1052 .join("_")
1053 }
1054 "snake_case" => {
1055 let mut out = String::new();
1057 for (i, ch) in s.chars().enumerate() {
1058 if ch.is_uppercase() && i > 0 {
1059 out.push('_');
1060 }
1061 out.push(ch.to_lowercase().next().unwrap_or(ch));
1062 }
1063 out
1064 }
1065 _ => s.to_string(), }
1067}
1068
1069fn decompose_hashes_field(
1074 rule: &mut SigmaRule,
1075 valid_algos: &[String],
1076 field_prefix: &str,
1077 drop_algo_prefix: bool,
1078) {
1079 for detection in rule.detection.named.values_mut() {
1080 decompose_hashes_in_detection(detection, valid_algos, field_prefix, drop_algo_prefix);
1081 }
1082}
1083
1084fn decompose_hashes_in_detection(
1085 detection: &mut Detection,
1086 valid_algos: &[String],
1087 field_prefix: &str,
1088 drop_algo_prefix: bool,
1089) {
1090 match detection {
1091 Detection::AllOf(items) => {
1092 let mut new_items: Vec<DetectionItem> = Vec::new();
1093 let mut i = 0;
1094 while i < items.len() {
1095 let item = &items[i];
1096 let is_hashes = item
1097 .field
1098 .name
1099 .as_deref()
1100 .map(|n| n.eq_ignore_ascii_case("hashes"))
1101 .unwrap_or(false);
1102
1103 if is_hashes {
1104 for val in &item.values {
1106 if let SigmaValue::String(s) = val {
1107 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1108 for pair in plain.split(',') {
1109 let pair = pair.trim();
1110 if let Some((algo, hash)) = pair.split_once('=') {
1111 let algo_upper = algo.trim().to_uppercase();
1112 if valid_algos.is_empty()
1113 || valid_algos
1114 .iter()
1115 .any(|a| a.eq_ignore_ascii_case(&algo_upper))
1116 {
1117 let field_name = if drop_algo_prefix {
1118 field_prefix.to_string()
1119 } else {
1120 format!("{field_prefix}{}", algo.trim())
1121 };
1122 new_items.push(DetectionItem {
1123 field: FieldSpec::new(
1124 Some(field_name),
1125 item.field.modifiers.clone(),
1126 ),
1127 values: vec![SigmaValue::String(SigmaString::new(
1128 hash.trim(),
1129 ))],
1130 });
1131 }
1132 }
1133 }
1134 }
1135 }
1136 } else {
1137 new_items.push(items[i].clone());
1138 }
1139 i += 1;
1140 }
1141 *items = new_items;
1142 }
1143 Detection::AnyOf(subs) => {
1144 for sub in subs.iter_mut() {
1145 decompose_hashes_in_detection(sub, valid_algos, field_prefix, drop_algo_prefix);
1146 }
1147 }
1148 Detection::Keywords(_) => {}
1149 }
1150}
1151
1152fn map_string_values(
1157 rule: &mut SigmaRule,
1158 state: &PipelineState,
1159 detection_conditions: &[DetectionItemCondition],
1160 field_name_conditions: &[FieldNameCondition],
1161 field_name_cond_not: bool,
1162 mapping: &HashMap<String, Vec<String>>,
1163) {
1164 for detection in rule.detection.named.values_mut() {
1165 map_strings_in_detection(
1166 detection,
1167 state,
1168 detection_conditions,
1169 field_name_conditions,
1170 field_name_cond_not,
1171 mapping,
1172 );
1173 }
1174}
1175
1176fn map_strings_in_detection(
1177 detection: &mut Detection,
1178 state: &PipelineState,
1179 detection_conditions: &[DetectionItemCondition],
1180 field_name_conditions: &[FieldNameCondition],
1181 field_name_cond_not: bool,
1182 mapping: &HashMap<String, Vec<String>>,
1183) {
1184 match detection {
1185 Detection::AllOf(items) => {
1186 for item in items.iter_mut() {
1187 if item_conditions_match(
1188 item,
1189 state,
1190 detection_conditions,
1191 field_name_conditions,
1192 field_name_cond_not,
1193 ) {
1194 map_string_expand_values(&mut item.values, mapping);
1195 }
1196 }
1197 }
1198 Detection::AnyOf(subs) => {
1199 for sub in subs.iter_mut() {
1200 map_strings_in_detection(
1201 sub,
1202 state,
1203 detection_conditions,
1204 field_name_conditions,
1205 field_name_cond_not,
1206 mapping,
1207 );
1208 }
1209 }
1210 Detection::Keywords(values) => {
1211 map_string_expand_values(values, mapping);
1212 }
1213 }
1214}
1215
1216fn map_string_expand_values(values: &mut Vec<SigmaValue>, mapping: &HashMap<String, Vec<String>>) {
1223 let mut extra: Vec<(usize, Vec<SigmaValue>)> = Vec::new();
1224
1225 for (i, val) in values.iter_mut().enumerate() {
1226 if let SigmaValue::String(s) = val {
1227 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1228 if let Some(replacements) = mapping.get(&plain) {
1229 if let Some(first) = replacements.first() {
1230 *s = SigmaString::new(first);
1231 }
1232 if replacements.len() > 1 {
1234 let extras: Vec<SigmaValue> = replacements[1..]
1235 .iter()
1236 .map(|r| SigmaValue::String(SigmaString::new(r)))
1237 .collect();
1238 extra.push((i, extras));
1239 }
1240 }
1241 }
1242 }
1243
1244 for (idx, extras) in extra.into_iter().rev() {
1246 for (j, v) in extras.into_iter().enumerate() {
1247 values.insert(idx + 1 + j, v);
1248 }
1249 }
1250}
1251
1252fn set_detection_item_values(
1257 rule: &mut SigmaRule,
1258 state: &PipelineState,
1259 detection_conditions: &[DetectionItemCondition],
1260 field_name_conditions: &[FieldNameCondition],
1261 field_name_cond_not: bool,
1262 value: &SigmaValue,
1263) {
1264 for detection in rule.detection.named.values_mut() {
1265 set_values_in_detection(
1266 detection,
1267 state,
1268 detection_conditions,
1269 field_name_conditions,
1270 field_name_cond_not,
1271 value,
1272 );
1273 }
1274}
1275
1276fn set_values_in_detection(
1277 detection: &mut Detection,
1278 state: &PipelineState,
1279 detection_conditions: &[DetectionItemCondition],
1280 field_name_conditions: &[FieldNameCondition],
1281 field_name_cond_not: bool,
1282 value: &SigmaValue,
1283) {
1284 match detection {
1285 Detection::AllOf(items) => {
1286 for item in items.iter_mut() {
1287 if item_conditions_match(
1288 item,
1289 state,
1290 detection_conditions,
1291 field_name_conditions,
1292 field_name_cond_not,
1293 ) {
1294 item.values = vec![value.clone()];
1295 }
1296 }
1297 }
1298 Detection::AnyOf(subs) => {
1299 for sub in subs.iter_mut() {
1300 set_values_in_detection(
1301 sub,
1302 state,
1303 detection_conditions,
1304 field_name_conditions,
1305 field_name_cond_not,
1306 value,
1307 );
1308 }
1309 }
1310 Detection::Keywords(_) => {}
1311 }
1312}
1313
1314fn convert_detection_item_types(
1319 rule: &mut SigmaRule,
1320 state: &PipelineState,
1321 detection_conditions: &[DetectionItemCondition],
1322 field_name_conditions: &[FieldNameCondition],
1323 field_name_cond_not: bool,
1324 target_type: &str,
1325) {
1326 for detection in rule.detection.named.values_mut() {
1327 convert_types_in_detection(
1328 detection,
1329 state,
1330 detection_conditions,
1331 field_name_conditions,
1332 field_name_cond_not,
1333 target_type,
1334 );
1335 }
1336}
1337
1338fn convert_types_in_detection(
1339 detection: &mut Detection,
1340 state: &PipelineState,
1341 detection_conditions: &[DetectionItemCondition],
1342 field_name_conditions: &[FieldNameCondition],
1343 field_name_cond_not: bool,
1344 target_type: &str,
1345) {
1346 match detection {
1347 Detection::AllOf(items) => {
1348 for item in items.iter_mut() {
1349 if item_conditions_match(
1350 item,
1351 state,
1352 detection_conditions,
1353 field_name_conditions,
1354 field_name_cond_not,
1355 ) {
1356 for val in item.values.iter_mut() {
1357 *val = convert_value(val, target_type);
1358 }
1359 }
1360 }
1361 }
1362 Detection::AnyOf(subs) => {
1363 for sub in subs.iter_mut() {
1364 convert_types_in_detection(
1365 sub,
1366 state,
1367 detection_conditions,
1368 field_name_conditions,
1369 field_name_cond_not,
1370 target_type,
1371 );
1372 }
1373 }
1374 Detection::Keywords(_) => {}
1375 }
1376}
1377
1378fn convert_value(val: &SigmaValue, target: &str) -> SigmaValue {
1379 match target {
1380 "str" | "string" => match val {
1381 SigmaValue::String(_) => val.clone(),
1382 SigmaValue::Integer(n) => SigmaValue::String(SigmaString::new(&n.to_string())),
1383 SigmaValue::Float(f) => SigmaValue::String(SigmaString::new(&f.to_string())),
1384 SigmaValue::Bool(b) => SigmaValue::String(SigmaString::new(&b.to_string())),
1385 SigmaValue::Null => SigmaValue::String(SigmaString::new("null")),
1386 },
1387 "int" | "integer" => match val {
1388 SigmaValue::String(s) => {
1389 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1390 plain
1391 .parse::<i64>()
1392 .map(SigmaValue::Integer)
1393 .unwrap_or_else(|_| val.clone())
1394 }
1395 SigmaValue::Float(f) => SigmaValue::Integer(*f as i64),
1396 SigmaValue::Bool(b) => SigmaValue::Integer(if *b { 1 } else { 0 }),
1397 _ => val.clone(),
1398 },
1399 "float" => match val {
1400 SigmaValue::String(s) => {
1401 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1402 plain
1403 .parse::<f64>()
1404 .map(SigmaValue::Float)
1405 .unwrap_or_else(|_| val.clone())
1406 }
1407 SigmaValue::Integer(n) => SigmaValue::Float(*n as f64),
1408 SigmaValue::Bool(b) => SigmaValue::Float(if *b { 1.0 } else { 0.0 }),
1409 _ => val.clone(),
1410 },
1411 "bool" | "boolean" => match val {
1412 SigmaValue::String(s) => {
1413 let plain = s.as_plain().unwrap_or_else(|| s.original.clone());
1414 match plain.to_lowercase().as_str() {
1415 "true" | "1" | "yes" => SigmaValue::Bool(true),
1416 "false" | "0" | "no" => SigmaValue::Bool(false),
1417 _ => val.clone(),
1418 }
1419 }
1420 SigmaValue::Integer(n) => SigmaValue::Bool(*n != 0),
1421 SigmaValue::Float(f) => SigmaValue::Bool(*f != 0.0),
1422 _ => val.clone(),
1423 },
1424 _ => val.clone(),
1425 }
1426}
1427
1428fn apply_case_transformation(
1433 rule: &mut SigmaRule,
1434 state: &PipelineState,
1435 detection_conditions: &[DetectionItemCondition],
1436 field_name_conditions: &[FieldNameCondition],
1437 field_name_cond_not: bool,
1438 case_type: &str,
1439) {
1440 for detection in rule.detection.named.values_mut() {
1441 apply_case_in_detection(
1442 detection,
1443 state,
1444 detection_conditions,
1445 field_name_conditions,
1446 field_name_cond_not,
1447 case_type,
1448 );
1449 }
1450}
1451
1452fn apply_case_in_detection(
1453 detection: &mut Detection,
1454 state: &PipelineState,
1455 detection_conditions: &[DetectionItemCondition],
1456 field_name_conditions: &[FieldNameCondition],
1457 field_name_cond_not: bool,
1458 case_type: &str,
1459) {
1460 match detection {
1461 Detection::AllOf(items) => {
1462 for item in items.iter_mut() {
1463 if item_conditions_match(
1464 item,
1465 state,
1466 detection_conditions,
1467 field_name_conditions,
1468 field_name_cond_not,
1469 ) {
1470 for val in item.values.iter_mut() {
1471 apply_case_to_value(val, case_type);
1472 }
1473 }
1474 }
1475 }
1476 Detection::AnyOf(subs) => {
1477 for sub in subs.iter_mut() {
1478 apply_case_in_detection(
1479 sub,
1480 state,
1481 detection_conditions,
1482 field_name_conditions,
1483 field_name_cond_not,
1484 case_type,
1485 );
1486 }
1487 }
1488 Detection::Keywords(values) => {
1489 for val in values.iter_mut() {
1490 apply_case_to_value(val, case_type);
1491 }
1492 }
1493 }
1494}
1495
1496fn apply_case_to_value(val: &mut SigmaValue, case_type: &str) {
1497 if let SigmaValue::String(s) = val {
1498 let transformed = match case_type {
1499 "lower" | "lowercase" => s.original.to_lowercase(),
1500 "upper" | "uppercase" => s.original.to_uppercase(),
1501 "snake_case" => apply_named_string_fn("snake_case", &s.original),
1502 _ => return,
1503 };
1504 if transformed != s.original {
1505 *s = SigmaString::new(&transformed);
1506 }
1507 }
1508}
1509
1510fn item_conditions_match(
1515 item: &DetectionItem,
1516 state: &PipelineState,
1517 detection_conditions: &[DetectionItemCondition],
1518 field_name_conditions: &[FieldNameCondition],
1519 field_name_cond_not: bool,
1520) -> bool {
1521 let det_match = detection_conditions.is_empty()
1522 || detection_conditions
1523 .iter()
1524 .all(|c| c.matches_item(item, state));
1525
1526 let field_match = if let Some(ref name) = item.field.name {
1527 field_conditions_match(name, state, field_name_conditions, field_name_cond_not)
1528 } else {
1529 field_name_conditions.is_empty()
1530 };
1531
1532 det_match && field_match
1533}
1534
1535fn rule_has_matching_item(
1540 rule: &SigmaRule,
1541 state: &PipelineState,
1542 conditions: &[DetectionItemCondition],
1543) -> bool {
1544 for detection in rule.detection.named.values() {
1545 if detection_has_matching_item(detection, state, conditions) {
1546 return true;
1547 }
1548 }
1549 false
1550}
1551
1552fn detection_has_matching_item(
1553 detection: &Detection,
1554 state: &PipelineState,
1555 conditions: &[DetectionItemCondition],
1556) -> bool {
1557 match detection {
1558 Detection::AllOf(items) => items
1559 .iter()
1560 .any(|item| conditions.iter().all(|c| c.matches_item(item, state))),
1561 Detection::AnyOf(subs) => subs
1562 .iter()
1563 .any(|sub| detection_has_matching_item(sub, state, conditions)),
1564 Detection::Keywords(_) => false,
1565 }
1566}
1567
1568#[cfg(test)]
1569mod tests {
1570 use super::*;
1571 use rsigma_parser::{Detections, LogSource, Modifier};
1572
1573 fn make_test_rule() -> SigmaRule {
1574 let mut named = HashMap::new();
1575 named.insert(
1576 "selection".to_string(),
1577 Detection::AllOf(vec![
1578 DetectionItem {
1579 field: FieldSpec::new(
1580 Some("CommandLine".to_string()),
1581 vec![Modifier::Contains],
1582 ),
1583 values: vec![SigmaValue::String(SigmaString::new("whoami"))],
1584 },
1585 DetectionItem {
1586 field: FieldSpec::new(
1587 Some("ParentImage".to_string()),
1588 vec![Modifier::EndsWith],
1589 ),
1590 values: vec![SigmaValue::String(SigmaString::new("\\cmd.exe"))],
1591 },
1592 ]),
1593 );
1594
1595 SigmaRule {
1596 title: "Test Rule".to_string(),
1597 logsource: LogSource {
1598 category: Some("process_creation".to_string()),
1599 product: Some("windows".to_string()),
1600 service: None,
1601 definition: None,
1602 custom: HashMap::new(),
1603 },
1604 detection: Detections {
1605 named,
1606 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
1607 condition_strings: vec!["selection".to_string()],
1608 timeframe: None,
1609 },
1610 id: Some("test-001".to_string()),
1611 name: None,
1612 related: vec![],
1613 taxonomy: None,
1614 status: None,
1615 description: None,
1616 license: None,
1617 author: None,
1618 references: vec![],
1619 date: None,
1620 modified: None,
1621 fields: vec![],
1622 falsepositives: vec![],
1623 level: Some(rsigma_parser::Level::Medium),
1624 tags: vec![],
1625 scope: vec![],
1626 custom_attributes: HashMap::new(),
1627 }
1628 }
1629
1630 #[test]
1631 fn test_field_name_mapping() {
1632 let mut rule = make_test_rule();
1633 let mut state = PipelineState::default();
1634 let mut mapping = HashMap::new();
1635 mapping.insert(
1636 "CommandLine".to_string(),
1637 vec!["process.command_line".to_string()],
1638 );
1639 mapping.insert(
1640 "ParentImage".to_string(),
1641 vec!["process.parent.executable".to_string()],
1642 );
1643
1644 let t = Transformation::FieldNameMapping { mapping };
1645 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1646
1647 let det = &rule.detection.named["selection"];
1648 if let Detection::AllOf(items) = det {
1649 assert_eq!(
1650 items[0].field.name,
1651 Some("process.command_line".to_string())
1652 );
1653 assert_eq!(
1654 items[1].field.name,
1655 Some("process.parent.executable".to_string())
1656 );
1657 } else {
1658 panic!("Expected AllOf");
1659 }
1660 }
1661
1662 #[test]
1663 fn test_field_name_mapping_one_to_many_expands_to_anyof() {
1664 let mut rule = make_test_rule();
1668 let mut state = PipelineState::default();
1669 let mut mapping = HashMap::new();
1670 mapping.insert(
1671 "CommandLine".to_string(),
1672 vec!["cmd_a".to_string(), "cmd_b".to_string()],
1673 );
1674
1675 let t = Transformation::FieldNameMapping { mapping };
1676 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1677
1678 let det = &rule.detection.named["selection"];
1679 let Detection::AnyOf(branches) = det else {
1680 panic!("Expected AnyOf, got {det:?}");
1681 };
1682 assert_eq!(branches.len(), 2);
1683
1684 let mut seen_first_fields: Vec<Option<String>> = Vec::new();
1685 for branch in branches {
1686 let Detection::AllOf(items) = branch else {
1687 panic!("Expected AllOf in each branch, got {branch:?}");
1688 };
1689 assert_eq!(items.len(), 2);
1690 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
1692 seen_first_fields.push(items[0].field.name.clone());
1693 }
1694 seen_first_fields.sort();
1695 assert_eq!(
1696 seen_first_fields,
1697 vec![Some("cmd_a".to_string()), Some("cmd_b".to_string())]
1698 );
1699 }
1700
1701 #[test]
1702 fn test_field_name_mapping_one_to_many_cartesian_when_two_items_expand() {
1703 let mut rule = make_test_rule();
1705 let mut state = PipelineState::default();
1706 let mut mapping = HashMap::new();
1707 mapping.insert(
1708 "CommandLine".to_string(),
1709 vec!["cmd_a".to_string(), "cmd_b".to_string()],
1710 );
1711 mapping.insert(
1712 "ParentImage".to_string(),
1713 vec!["parent_x".to_string(), "parent_y".to_string()],
1714 );
1715
1716 let t = Transformation::FieldNameMapping { mapping };
1717 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1718
1719 let det = &rule.detection.named["selection"];
1720 let Detection::AnyOf(branches) = det else {
1721 panic!("Expected AnyOf, got {det:?}");
1722 };
1723 assert_eq!(branches.len(), 4);
1724
1725 let mut combos: Vec<(Option<String>, Option<String>)> = branches
1726 .iter()
1727 .map(|b| {
1728 let Detection::AllOf(items) = b else {
1729 panic!("Expected AllOf");
1730 };
1731 (items[0].field.name.clone(), items[1].field.name.clone())
1732 })
1733 .collect();
1734 combos.sort();
1735 assert_eq!(
1736 combos,
1737 vec![
1738 (Some("cmd_a".to_string()), Some("parent_x".to_string())),
1739 (Some("cmd_a".to_string()), Some("parent_y".to_string())),
1740 (Some("cmd_b".to_string()), Some("parent_x".to_string())),
1741 (Some("cmd_b".to_string()), Some("parent_y".to_string())),
1742 ]
1743 );
1744 }
1745
1746 #[test]
1747 fn test_field_name_mapping_cartesian_expansion_capped() {
1748 use rsigma_parser::{Detection, Detections, FieldSpec, LogSource, Modifier};
1753 let alts: Vec<String> = (0..7).map(|i| format!("alt_{i}")).collect();
1754 let mut mapping = HashMap::new();
1755 let mut items = Vec::new();
1756 for i in 0..5 {
1757 let name = format!("Field{i}");
1758 mapping.insert(name.clone(), alts.clone());
1759 items.push(DetectionItem {
1760 field: FieldSpec::new(Some(name), vec![Modifier::Contains]),
1761 values: vec![SigmaValue::String(SigmaString::new("x"))],
1762 });
1763 }
1764 let mut named = HashMap::new();
1765 named.insert("selection".to_string(), Detection::AllOf(items));
1766 let mut rule = SigmaRule {
1767 title: "Cartesian Bomb".to_string(),
1768 logsource: LogSource {
1769 category: None,
1770 product: None,
1771 service: None,
1772 definition: None,
1773 custom: HashMap::new(),
1774 },
1775 detection: Detections {
1776 named,
1777 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
1778 condition_strings: vec!["selection".to_string()],
1779 timeframe: None,
1780 },
1781 id: None,
1782 name: None,
1783 related: vec![],
1784 taxonomy: None,
1785 status: None,
1786 description: None,
1787 license: None,
1788 author: None,
1789 references: vec![],
1790 date: None,
1791 modified: None,
1792 fields: vec![],
1793 falsepositives: vec![],
1794 level: None,
1795 tags: vec![],
1796 scope: vec![],
1797 custom_attributes: HashMap::new(),
1798 };
1799
1800 let mut state = PipelineState::default();
1801 let t = Transformation::FieldNameMapping { mapping };
1802 let err = t
1803 .apply(&mut rule, &mut state, &[], &[], false)
1804 .expect_err("expansion above cap must error");
1805 let msg = format!("{err}");
1806 assert!(msg.contains("16807"), "expected total in error: {msg}");
1807 assert!(msg.contains("4096"), "expected limit in error: {msg}");
1808 assert!(
1809 msg.contains("Cartesian Bomb"),
1810 "expected rule title in error: {msg}"
1811 );
1812 }
1813
1814 #[test]
1815 fn test_field_name_mapping_single_alternative_in_list_uses_fast_path() {
1816 let mut rule = make_test_rule();
1819 let mut state = PipelineState::default();
1820 let mut mapping = HashMap::new();
1821 mapping.insert("CommandLine".to_string(), vec!["cmd".to_string()]);
1822
1823 let t = Transformation::FieldNameMapping { mapping };
1824 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1825
1826 let det = &rule.detection.named["selection"];
1827 let Detection::AllOf(items) = det else {
1828 panic!("Expected AllOf (no expansion), got {det:?}");
1829 };
1830 assert_eq!(items[0].field.name, Some("cmd".to_string()));
1831 }
1832
1833 #[test]
1834 fn test_field_name_prefix() {
1835 let mut rule = make_test_rule();
1836 let mut state = PipelineState::default();
1837 let t = Transformation::FieldNamePrefix {
1838 prefix: "winlog.event_data.".to_string(),
1839 };
1840 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1841
1842 let det = &rule.detection.named["selection"];
1843 if let Detection::AllOf(items) = det {
1844 assert_eq!(
1845 items[0].field.name,
1846 Some("winlog.event_data.CommandLine".to_string())
1847 );
1848 } else {
1849 panic!("Expected AllOf");
1850 }
1851 }
1852
1853 #[test]
1854 fn test_field_name_suffix() {
1855 let mut rule = make_test_rule();
1856 let mut state = PipelineState::default();
1857 let t = Transformation::FieldNameSuffix {
1858 suffix: ".keyword".to_string(),
1859 };
1860 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1861
1862 let det = &rule.detection.named["selection"];
1863 if let Detection::AllOf(items) = det {
1864 assert_eq!(items[0].field.name, Some("CommandLine.keyword".to_string()));
1865 } else {
1866 panic!("Expected AllOf");
1867 }
1868 }
1869
1870 #[test]
1871 fn test_change_logsource() {
1872 let mut rule = make_test_rule();
1873 let mut state = PipelineState::default();
1874 let t = Transformation::ChangeLogsource {
1875 category: Some("endpoint".to_string()),
1876 product: Some("elastic".to_string()),
1877 service: None,
1878 };
1879 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1880
1881 assert_eq!(rule.logsource.category, Some("endpoint".to_string()));
1882 assert_eq!(rule.logsource.product, Some("elastic".to_string()));
1883 }
1884
1885 #[test]
1886 fn test_replace_string() {
1887 let mut rule = make_test_rule();
1888 let mut state = PipelineState::default();
1889 let t = Transformation::ReplaceString {
1890 regex: r"whoami".to_string(),
1891 replacement: "REPLACED".to_string(),
1892 skip_special: false,
1893 };
1894 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1895
1896 let det = &rule.detection.named["selection"];
1897 if let Detection::AllOf(items) = det {
1898 if let SigmaValue::String(s) = &items[0].values[0] {
1899 assert_eq!(s.original, "REPLACED");
1900 } else {
1901 panic!("Expected String value");
1902 }
1903 } else {
1904 panic!("Expected AllOf");
1905 }
1906 }
1907
1908 #[test]
1909 fn test_add_condition() {
1910 let mut rule = make_test_rule();
1911 let mut state = PipelineState::default();
1912 let mut conds = HashMap::new();
1913 conds.insert(
1914 "index".to_string(),
1915 SigmaValue::String(SigmaString::new("windows-*")),
1916 );
1917 let t = Transformation::AddCondition {
1918 conditions: conds,
1919 negated: false,
1920 };
1921 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1922
1923 assert!(
1925 rule.detection
1926 .named
1927 .keys()
1928 .any(|k| k.starts_with("__pipeline_cond_"))
1929 );
1930 assert_eq!(rule.detection.conditions.len(), 1);
1932 if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
1933 assert_eq!(parts.len(), 2);
1934 } else {
1935 panic!("Expected And condition");
1936 }
1937 }
1938
1939 #[test]
1940 fn test_set_state() {
1941 let mut rule = make_test_rule();
1942 let mut state = PipelineState::default();
1943 let t = Transformation::SetState {
1944 key: "index".to_string(),
1945 value: "windows".to_string(),
1946 };
1947 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
1948 assert!(state.state_matches("index", "windows"));
1949 }
1950
1951 #[test]
1952 fn test_drop_detection_item_with_field_condition() {
1953 let mut rule = make_test_rule();
1954 let mut state = PipelineState::default();
1955
1956 let field_conds = vec![FieldNameCondition::IncludeFields {
1957 matcher: super::super::conditions::FieldMatcher::Plain(vec!["ParentImage".to_string()]),
1958 }];
1959
1960 let t = Transformation::DropDetectionItem;
1961 t.apply(&mut rule, &mut state, &[], &field_conds, false)
1962 .unwrap();
1963
1964 let det = &rule.detection.named["selection"];
1965 if let Detection::AllOf(items) = det {
1966 assert_eq!(items.len(), 1); assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
1968 } else {
1969 panic!("Expected AllOf");
1970 }
1971 }
1972
1973 #[test]
1974 fn test_field_name_mapping_with_conditions() {
1975 let mut rule = make_test_rule();
1976 let mut state = PipelineState::default();
1977
1978 let field_conds = vec![FieldNameCondition::IncludeFields {
1980 matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
1981 }];
1982
1983 let mut mapping = HashMap::new();
1984 mapping.insert("CommandLine".to_string(), vec!["process.args".to_string()]);
1985 mapping.insert(
1986 "ParentImage".to_string(),
1987 vec!["process.parent".to_string()],
1988 );
1989
1990 let t = Transformation::FieldNameMapping { mapping };
1991 t.apply(&mut rule, &mut state, &[], &field_conds, false)
1992 .unwrap();
1993
1994 let det = &rule.detection.named["selection"];
1995 if let Detection::AllOf(items) = det {
1996 assert_eq!(items[0].field.name, Some("process.args".to_string()));
1997 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
1999 } else {
2000 panic!("Expected AllOf");
2001 }
2002 }
2003
2004 #[test]
2005 fn test_rule_failure() {
2006 let mut rule = make_test_rule();
2007 let mut state = PipelineState::default();
2008 let t = Transformation::RuleFailure {
2009 message: "Unsupported rule".to_string(),
2010 };
2011 let result = t.apply(&mut rule, &mut state, &[], &[], false);
2012 assert!(result.is_err());
2013 }
2014
2015 #[test]
2016 fn test_value_placeholders() {
2017 let mut named = HashMap::new();
2018 named.insert(
2019 "selection".to_string(),
2020 Detection::AllOf(vec![DetectionItem {
2021 field: FieldSpec::new(Some("User".to_string()), vec![]),
2022 values: vec![SigmaValue::String(SigmaString::new("%admin_users%"))],
2023 }]),
2024 );
2025
2026 let mut rule = SigmaRule {
2027 title: "Test".to_string(),
2028 logsource: LogSource::default(),
2029 detection: Detections {
2030 named,
2031 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2032 condition_strings: vec!["selection".to_string()],
2033 timeframe: None,
2034 },
2035 id: None,
2036 name: None,
2037 related: vec![],
2038 taxonomy: None,
2039 status: None,
2040 description: None,
2041 license: None,
2042 author: None,
2043 references: vec![],
2044 date: None,
2045 modified: None,
2046 fields: vec![],
2047 falsepositives: vec![],
2048 level: None,
2049 tags: vec![],
2050 scope: vec![],
2051 custom_attributes: HashMap::new(),
2052 };
2053
2054 let mut state = PipelineState::default();
2055 state.vars.insert(
2056 "admin_users".to_string(),
2057 vec!["root".to_string(), "admin".to_string()],
2058 );
2059
2060 let t = Transformation::ValuePlaceholders;
2061 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2062
2063 let det = &rule.detection.named["selection"];
2064 if let Detection::AllOf(items) = det {
2065 assert_eq!(items[0].values.len(), 2);
2067 } else {
2068 panic!("Expected AllOf");
2069 }
2070 }
2071
2072 #[test]
2073 fn test_field_name_transform_lowercase() {
2074 let mut rule = make_test_rule();
2075 let mut state = PipelineState::default();
2076 let t = Transformation::FieldNameTransform {
2077 transform_func: "lower".to_string(),
2078 mapping: HashMap::new(),
2079 };
2080 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2081
2082 let det = &rule.detection.named["selection"];
2083 if let Detection::AllOf(items) = det {
2084 assert_eq!(items[0].field.name, Some("commandline".to_string()));
2085 assert_eq!(items[1].field.name, Some("parentimage".to_string()));
2086 } else {
2087 panic!("Expected AllOf");
2088 }
2089 }
2090
2091 #[test]
2092 fn test_field_name_transform_with_mapping_override() {
2093 let mut rule = make_test_rule();
2094 let mut state = PipelineState::default();
2095 let mut mapping = HashMap::new();
2096 mapping.insert("CommandLine".to_string(), "cmd_line".to_string());
2097 let t = Transformation::FieldNameTransform {
2098 transform_func: "lower".to_string(),
2099 mapping,
2100 };
2101 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2102
2103 let det = &rule.detection.named["selection"];
2104 if let Detection::AllOf(items) = det {
2105 assert_eq!(items[0].field.name, Some("cmd_line".to_string()));
2107 assert_eq!(items[1].field.name, Some("parentimage".to_string()));
2109 } else {
2110 panic!("Expected AllOf");
2111 }
2112 }
2113
2114 #[test]
2115 fn test_field_name_transform_snake_case() {
2116 let mut rule = make_test_rule();
2117 let mut state = PipelineState::default();
2118 let t = Transformation::FieldNameTransform {
2119 transform_func: "snake_case".to_string(),
2120 mapping: HashMap::new(),
2121 };
2122 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2123
2124 let det = &rule.detection.named["selection"];
2125 if let Detection::AllOf(items) = det {
2126 assert_eq!(items[0].field.name, Some("command_line".to_string()));
2127 assert_eq!(items[1].field.name, Some("parent_image".to_string()));
2128 } else {
2129 panic!("Expected AllOf");
2130 }
2131 }
2132
2133 #[test]
2134 fn test_hashes_fields_decomposition() {
2135 let mut named = HashMap::new();
2136 named.insert(
2137 "selection".to_string(),
2138 Detection::AllOf(vec![DetectionItem {
2139 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2140 values: vec![SigmaValue::String(SigmaString::new(
2141 "SHA1=abc123,MD5=def456",
2142 ))],
2143 }]),
2144 );
2145
2146 let mut rule = make_test_rule();
2147 rule.detection.named = named;
2148
2149 let mut state = PipelineState::default();
2150 let t = Transformation::HashesFields {
2151 valid_hash_algos: vec!["SHA1".to_string(), "MD5".to_string()],
2152 field_prefix: "File".to_string(),
2153 drop_algo_prefix: false,
2154 };
2155 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2156
2157 let det = &rule.detection.named["selection"];
2158 if let Detection::AllOf(items) = det {
2159 assert_eq!(items.len(), 2);
2160 assert_eq!(items[0].field.name, Some("FileSHA1".to_string()));
2161 assert_eq!(items[1].field.name, Some("FileMD5".to_string()));
2162 if let SigmaValue::String(s) = &items[0].values[0] {
2163 assert_eq!(s.original, "abc123");
2164 }
2165 if let SigmaValue::String(s) = &items[1].values[0] {
2166 assert_eq!(s.original, "def456");
2167 }
2168 } else {
2169 panic!("Expected AllOf");
2170 }
2171 }
2172
2173 #[test]
2174 fn test_map_string() {
2175 let mut rule = make_test_rule();
2176 let mut state = PipelineState::default();
2177 let mut mapping = HashMap::new();
2178 mapping.insert("whoami".to_string(), vec!["who_am_i".to_string()]);
2179 let t = Transformation::MapString { mapping };
2180 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2181
2182 let det = &rule.detection.named["selection"];
2183 if let Detection::AllOf(items) = det {
2184 if let SigmaValue::String(s) = &items[0].values[0] {
2185 assert_eq!(s.original, "who_am_i");
2186 } else {
2187 panic!("Expected String value");
2188 }
2189 } else {
2190 panic!("Expected AllOf");
2191 }
2192 }
2193
2194 #[test]
2195 fn test_map_string_no_match() {
2196 let mut rule = make_test_rule();
2197 let mut state = PipelineState::default();
2198 let mut mapping = HashMap::new();
2199 mapping.insert("nonexistent".to_string(), vec!["replaced".to_string()]);
2200 let t = Transformation::MapString { mapping };
2201 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2202
2203 let det = &rule.detection.named["selection"];
2205 if let Detection::AllOf(items) = det
2206 && let SigmaValue::String(s) = &items[0].values[0]
2207 {
2208 assert_eq!(s.original, "whoami");
2209 }
2210 }
2211
2212 #[test]
2213 fn test_set_value() {
2214 let mut rule = make_test_rule();
2215 let mut state = PipelineState::default();
2216 let t = Transformation::SetValue {
2217 value: SigmaValue::String(SigmaString::new("FIXED")),
2218 };
2219 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2220
2221 let det = &rule.detection.named["selection"];
2222 if let Detection::AllOf(items) = det {
2223 for item in items {
2224 assert_eq!(item.values.len(), 1);
2225 if let SigmaValue::String(s) = &item.values[0] {
2226 assert_eq!(s.original, "FIXED");
2227 }
2228 }
2229 } else {
2230 panic!("Expected AllOf");
2231 }
2232 }
2233
2234 #[test]
2235 fn test_convert_type_string_to_int() {
2236 let mut named = HashMap::new();
2237 named.insert(
2238 "selection".to_string(),
2239 Detection::AllOf(vec![DetectionItem {
2240 field: FieldSpec::new(Some("EventID".to_string()), vec![]),
2241 values: vec![SigmaValue::String(SigmaString::new("4688"))],
2242 }]),
2243 );
2244 let mut rule = make_test_rule();
2245 rule.detection.named = named;
2246
2247 let mut state = PipelineState::default();
2248 let t = Transformation::ConvertType {
2249 target_type: "int".to_string(),
2250 };
2251 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2252
2253 let det = &rule.detection.named["selection"];
2254 if let Detection::AllOf(items) = det {
2255 assert!(matches!(items[0].values[0], SigmaValue::Integer(4688)));
2256 } else {
2257 panic!("Expected AllOf");
2258 }
2259 }
2260
2261 #[test]
2262 fn test_convert_type_int_to_string() {
2263 let mut named = HashMap::new();
2264 named.insert(
2265 "selection".to_string(),
2266 Detection::AllOf(vec![DetectionItem {
2267 field: FieldSpec::new(Some("EventID".to_string()), vec![]),
2268 values: vec![SigmaValue::Integer(4688)],
2269 }]),
2270 );
2271 let mut rule = make_test_rule();
2272 rule.detection.named = named;
2273
2274 let mut state = PipelineState::default();
2275 let t = Transformation::ConvertType {
2276 target_type: "str".to_string(),
2277 };
2278 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2279
2280 let det = &rule.detection.named["selection"];
2281 if let Detection::AllOf(items) = det {
2282 if let SigmaValue::String(s) = &items[0].values[0] {
2283 assert_eq!(s.original, "4688");
2284 } else {
2285 panic!("Expected String");
2286 }
2287 }
2288 }
2289
2290 #[test]
2291 fn test_convert_type_to_bool() {
2292 let mut named = HashMap::new();
2293 named.insert(
2294 "selection".to_string(),
2295 Detection::AllOf(vec![DetectionItem {
2296 field: FieldSpec::new(Some("Enabled".to_string()), vec![]),
2297 values: vec![SigmaValue::String(SigmaString::new("true"))],
2298 }]),
2299 );
2300 let mut rule = make_test_rule();
2301 rule.detection.named = named;
2302
2303 let mut state = PipelineState::default();
2304 let t = Transformation::ConvertType {
2305 target_type: "bool".to_string(),
2306 };
2307 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2308
2309 let det = &rule.detection.named["selection"];
2310 if let Detection::AllOf(items) = det {
2311 assert!(matches!(items[0].values[0], SigmaValue::Bool(true)));
2312 }
2313 }
2314
2315 #[test]
2316 fn test_regex_noop() {
2317 let mut rule = make_test_rule();
2318 let mut state = PipelineState::default();
2319 let t = Transformation::Regex;
2320 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2321 assert!(!result); }
2323
2324 #[test]
2325 fn test_add_field() {
2326 let mut rule = make_test_rule();
2327 assert!(rule.fields.is_empty());
2328
2329 let mut state = PipelineState::default();
2330 let t = Transformation::AddField {
2331 field: "EventID".to_string(),
2332 };
2333 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2334 assert_eq!(rule.fields, vec!["EventID".to_string()]);
2335
2336 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2338 assert_eq!(rule.fields, vec!["EventID".to_string()]);
2339 }
2340
2341 #[test]
2342 fn test_remove_field() {
2343 let mut rule = make_test_rule();
2344 rule.fields = vec!["EventID".to_string(), "CommandLine".to_string()];
2345
2346 let mut state = PipelineState::default();
2347 let t = Transformation::RemoveField {
2348 field: "EventID".to_string(),
2349 };
2350 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2351 assert_eq!(rule.fields, vec!["CommandLine".to_string()]);
2352 }
2353
2354 #[test]
2355 fn test_set_field() {
2356 let mut rule = make_test_rule();
2357 rule.fields = vec!["old".to_string()];
2358
2359 let mut state = PipelineState::default();
2360 let t = Transformation::SetField {
2361 fields: vec!["new1".to_string(), "new2".to_string()],
2362 };
2363 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2364 assert_eq!(rule.fields, vec!["new1".to_string(), "new2".to_string()]);
2365 }
2366
2367 #[test]
2368 fn test_set_custom_attribute() {
2369 let mut rule = make_test_rule();
2370 let mut state = PipelineState::default();
2371 let t = Transformation::SetCustomAttribute {
2372 attribute: "custom.key".to_string(),
2373 value: "custom_value".to_string(),
2374 };
2375 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2376 assert!(result);
2377 assert_eq!(
2378 rule.custom_attributes
2379 .get("custom.key")
2380 .and_then(|v| v.as_str()),
2381 Some("custom_value")
2382 );
2383 }
2384
2385 #[test]
2386 fn test_case_transformation_lower() {
2387 let mut rule = make_test_rule();
2388 let mut state = PipelineState::default();
2389 let t = Transformation::CaseTransformation {
2390 case_type: "lower".to_string(),
2391 };
2392 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2393
2394 let det = &rule.detection.named["selection"];
2395 if let Detection::AllOf(items) = det {
2396 if let SigmaValue::String(s) = &items[0].values[0] {
2398 assert_eq!(s.original, "whoami");
2399 }
2400 if let SigmaValue::String(s) = &items[1].values[0] {
2402 assert_eq!(s.original, "\\cmd.exe");
2403 }
2404 }
2405 }
2406
2407 #[test]
2408 fn test_case_transformation_upper() {
2409 let mut rule = make_test_rule();
2410 let mut state = PipelineState::default();
2411 let t = Transformation::CaseTransformation {
2412 case_type: "upper".to_string(),
2413 };
2414 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2415
2416 let det = &rule.detection.named["selection"];
2417 if let Detection::AllOf(items) = det {
2418 if let SigmaValue::String(s) = &items[0].values[0] {
2419 assert_eq!(s.original, "WHOAMI");
2420 }
2421 if let SigmaValue::String(s) = &items[1].values[0] {
2422 assert_eq!(s.original, "\\CMD.EXE");
2423 }
2424 }
2425 }
2426
2427 #[test]
2428 fn test_nest_transformation() {
2429 let mut rule = make_test_rule();
2430 let mut state = PipelineState::default();
2431
2432 let items = vec![
2434 super::super::TransformationItem {
2435 id: Some("inner_prefix".to_string()),
2436 transformation: Transformation::FieldNamePrefix {
2437 prefix: "winlog.".to_string(),
2438 },
2439 rule_conditions: vec![],
2440 rule_cond_expr: None,
2441 detection_item_conditions: vec![],
2442 field_name_conditions: vec![],
2443 field_name_cond_not: false,
2444 },
2445 super::super::TransformationItem {
2446 id: Some("inner_suffix".to_string()),
2447 transformation: Transformation::FieldNameSuffix {
2448 suffix: ".keyword".to_string(),
2449 },
2450 rule_conditions: vec![],
2451 rule_cond_expr: None,
2452 detection_item_conditions: vec![],
2453 field_name_conditions: vec![],
2454 field_name_cond_not: false,
2455 },
2456 ];
2457
2458 let t = Transformation::Nest { items };
2459 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2460
2461 let det = &rule.detection.named["selection"];
2462 if let Detection::AllOf(items) = det {
2463 assert_eq!(
2464 items[0].field.name,
2465 Some("winlog.CommandLine.keyword".to_string())
2466 );
2467 assert_eq!(
2468 items[1].field.name,
2469 Some("winlog.ParentImage.keyword".to_string())
2470 );
2471 } else {
2472 panic!("Expected AllOf");
2473 }
2474
2475 assert!(state.was_applied("inner_prefix"));
2477 assert!(state.was_applied("inner_suffix"));
2478 }
2479
2480 #[test]
2485 fn test_field_name_prefix_mapping() {
2486 let mut rule = make_test_rule();
2487 let mut state = PipelineState::default();
2488 let mut mapping = HashMap::new();
2489 mapping.insert("Command".to_string(), "process.".to_string());
2490 mapping.insert("Parent".to_string(), "process.parent.".to_string());
2491
2492 let t = Transformation::FieldNamePrefixMapping { mapping };
2493 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2494
2495 let det = &rule.detection.named["selection"];
2496 if let Detection::AllOf(items) = det {
2497 assert_eq!(items[0].field.name, Some("process.Line".to_string()));
2499 assert_eq!(
2501 items[1].field.name,
2502 Some("process.parent.Image".to_string())
2503 );
2504 } else {
2505 panic!("Expected AllOf");
2506 }
2507 }
2508
2509 #[test]
2510 fn test_field_name_prefix_mapping_no_match() {
2511 let mut rule = make_test_rule();
2512 let mut state = PipelineState::default();
2513 let mut mapping = HashMap::new();
2514 mapping.insert("NoMatch".to_string(), "replaced.".to_string());
2515
2516 let t = Transformation::FieldNamePrefixMapping { mapping };
2517 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2518
2519 let det = &rule.detection.named["selection"];
2521 if let Detection::AllOf(items) = det {
2522 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2523 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
2524 } else {
2525 panic!("Expected AllOf");
2526 }
2527 }
2528
2529 #[test]
2530 fn test_wildcard_placeholders_replaces_unresolved() {
2531 let mut named = HashMap::new();
2532 named.insert(
2533 "selection".to_string(),
2534 Detection::AllOf(vec![DetectionItem {
2535 field: FieldSpec::new(Some("User".to_string()), vec![]),
2536 values: vec![SigmaValue::String(SigmaString::new("%unknown_var%"))],
2537 }]),
2538 );
2539
2540 let mut rule = SigmaRule {
2541 title: "Test".to_string(),
2542 logsource: LogSource::default(),
2543 detection: Detections {
2544 named,
2545 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2546 condition_strings: vec!["selection".to_string()],
2547 timeframe: None,
2548 },
2549 id: None,
2550 name: None,
2551 related: vec![],
2552 taxonomy: None,
2553 status: None,
2554 description: None,
2555 license: None,
2556 author: None,
2557 references: vec![],
2558 date: None,
2559 modified: None,
2560 fields: vec![],
2561 falsepositives: vec![],
2562 level: None,
2563 tags: vec![],
2564 scope: vec![],
2565 custom_attributes: HashMap::new(),
2566 };
2567
2568 let mut state = PipelineState::default();
2569 let t = Transformation::WildcardPlaceholders;
2571 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2572
2573 let det = &rule.detection.named["selection"];
2574 if let Detection::AllOf(items) = det {
2575 if let SigmaValue::String(s) = &items[0].values[0] {
2576 assert_eq!(s.original, "*", "unresolved placeholder should become *");
2577 } else {
2578 panic!("Expected String value");
2579 }
2580 } else {
2581 panic!("Expected AllOf");
2582 }
2583 }
2584
2585 #[test]
2586 fn test_wildcard_placeholders_with_known_var() {
2587 let mut named = HashMap::new();
2588 named.insert(
2589 "selection".to_string(),
2590 Detection::AllOf(vec![DetectionItem {
2591 field: FieldSpec::new(Some("User".to_string()), vec![]),
2592 values: vec![SigmaValue::String(SigmaString::new("%admin%"))],
2593 }]),
2594 );
2595
2596 let mut rule = SigmaRule {
2597 title: "Test".to_string(),
2598 logsource: LogSource::default(),
2599 detection: Detections {
2600 named,
2601 conditions: vec![ConditionExpr::Identifier("selection".to_string())],
2602 condition_strings: vec!["selection".to_string()],
2603 timeframe: None,
2604 },
2605 id: None,
2606 name: None,
2607 related: vec![],
2608 taxonomy: None,
2609 status: None,
2610 description: None,
2611 license: None,
2612 author: None,
2613 references: vec![],
2614 date: None,
2615 modified: None,
2616 fields: vec![],
2617 falsepositives: vec![],
2618 level: None,
2619 tags: vec![],
2620 scope: vec![],
2621 custom_attributes: HashMap::new(),
2622 };
2623
2624 let mut state = PipelineState::default();
2625 state
2626 .vars
2627 .insert("admin".to_string(), vec!["root".to_string()]);
2628
2629 let t = Transformation::WildcardPlaceholders;
2631 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2632
2633 let det = &rule.detection.named["selection"];
2634 if let Detection::AllOf(items) = det {
2635 if let SigmaValue::String(s) = &items[0].values[0] {
2636 assert_eq!(s.original, "root");
2637 } else {
2638 panic!("Expected String value");
2639 }
2640 } else {
2641 panic!("Expected AllOf");
2642 }
2643 }
2644
2645 #[test]
2646 fn test_detection_item_failure_fires_on_match() {
2647 let mut rule = make_test_rule();
2648 let mut state = PipelineState::default();
2649
2650 let det_conds = vec![DetectionItemCondition::MatchString {
2652 regex: regex::Regex::new("whoami").unwrap(),
2653 negate: false,
2654 }];
2655
2656 let t = Transformation::DetectionItemFailure {
2657 message: "Unsupported detection item".to_string(),
2658 };
2659 let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
2660 assert!(result.is_err());
2661 let err = result.unwrap_err().to_string();
2662 assert!(err.contains("Unsupported detection item"));
2663 }
2664
2665 #[test]
2666 fn test_detection_item_failure_skips_on_no_match() {
2667 let mut rule = make_test_rule();
2668 let mut state = PipelineState::default();
2669
2670 let det_conds = vec![DetectionItemCondition::MatchString {
2672 regex: regex::Regex::new("nonexistent_value").unwrap(),
2673 negate: false,
2674 }];
2675
2676 let t = Transformation::DetectionItemFailure {
2677 message: "Should not fire".to_string(),
2678 };
2679 let result = t.apply(&mut rule, &mut state, &det_conds, &[], false);
2680 assert!(result.is_ok());
2681 assert!(!result.unwrap()); }
2683
2684 #[test]
2685 fn test_query_expression_placeholders_stores_in_state() {
2686 let mut rule = make_test_rule();
2687 let mut state = PipelineState::default();
2688 let t = Transformation::QueryExpressionPlaceholders {
2689 expression: "{field}={value}".to_string(),
2690 };
2691 let result = t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2692 assert!(result);
2693 let stored = state.get_state("query_expression_template").unwrap();
2694 assert_eq!(stored.as_str().unwrap(), "{field}={value}");
2695 }
2696
2697 #[test]
2702 fn test_add_condition_negated() {
2703 let mut rule = make_test_rule();
2704 let mut state = PipelineState::default();
2705 let mut conds = HashMap::new();
2706 conds.insert(
2707 "User".to_string(),
2708 SigmaValue::String(SigmaString::new("SYSTEM")),
2709 );
2710 let t = Transformation::AddCondition {
2711 conditions: conds,
2712 negated: true,
2713 };
2714 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2715
2716 assert_eq!(rule.detection.conditions.len(), 1);
2718 if let ConditionExpr::And(parts) = &rule.detection.conditions[0] {
2719 assert_eq!(parts.len(), 2);
2720 assert!(
2722 matches!(&parts[1], ConditionExpr::Not(_)),
2723 "Expected negated condition, got: {:?}",
2724 parts[1]
2725 );
2726 } else {
2727 panic!("Expected And condition");
2728 }
2729 }
2730
2731 #[test]
2736 fn test_replace_string_with_detection_item_condition() {
2737 let mut rule = make_test_rule();
2738 let mut state = PipelineState::default();
2739
2740 let det_conds = vec![DetectionItemCondition::MatchString {
2742 regex: regex::Regex::new("whoami").unwrap(),
2743 negate: false,
2744 }];
2745
2746 let t = Transformation::ReplaceString {
2747 regex: r"whoami".to_string(),
2748 replacement: "REPLACED".to_string(),
2749 skip_special: false,
2750 };
2751 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2752 .unwrap();
2753
2754 let det = &rule.detection.named["selection"];
2755 if let Detection::AllOf(items) = det {
2756 if let SigmaValue::String(s) = &items[0].values[0] {
2758 assert_eq!(s.original, "REPLACED");
2759 }
2760 if let SigmaValue::String(s) = &items[1].values[0] {
2762 assert_eq!(s.original, "\\cmd.exe");
2763 }
2764 } else {
2765 panic!("Expected AllOf");
2766 }
2767 }
2768
2769 #[test]
2770 fn test_set_value_with_is_null_condition() {
2771 let mut named = HashMap::new();
2773 named.insert(
2774 "selection".to_string(),
2775 Detection::AllOf(vec![
2776 DetectionItem {
2777 field: FieldSpec::new(Some("FieldA".to_string()), vec![]),
2778 values: vec![SigmaValue::Null],
2779 },
2780 DetectionItem {
2781 field: FieldSpec::new(Some("FieldB".to_string()), vec![]),
2782 values: vec![SigmaValue::String(SigmaString::new("value"))],
2783 },
2784 ]),
2785 );
2786
2787 let mut rule = make_test_rule();
2788 rule.detection.named = named;
2789 let mut state = PipelineState::default();
2790
2791 let det_conds = vec![DetectionItemCondition::IsNull { negate: false }];
2793
2794 let t = Transformation::SetValue {
2795 value: SigmaValue::String(SigmaString::new("DEFAULT")),
2796 };
2797 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2798 .unwrap();
2799
2800 let det = &rule.detection.named["selection"];
2801 if let Detection::AllOf(items) = det {
2802 if let SigmaValue::String(s) = &items[0].values[0] {
2804 assert_eq!(s.original, "DEFAULT");
2805 } else {
2806 panic!("Expected String after set_value on null");
2807 }
2808 if let SigmaValue::String(s) = &items[1].values[0] {
2810 assert_eq!(s.original, "value");
2811 }
2812 } else {
2813 panic!("Expected AllOf");
2814 }
2815 }
2816
2817 #[test]
2818 fn test_drop_detection_item_with_match_string_condition() {
2819 let mut rule = make_test_rule();
2820 let mut state = PipelineState::default();
2821
2822 let det_conds = vec![DetectionItemCondition::MatchString {
2824 regex: regex::Regex::new("whoami").unwrap(),
2825 negate: false,
2826 }];
2827
2828 let t = Transformation::DropDetectionItem;
2829 t.apply(&mut rule, &mut state, &det_conds, &[], false)
2830 .unwrap();
2831
2832 let det = &rule.detection.named["selection"];
2833 if let Detection::AllOf(items) = det {
2834 assert_eq!(items.len(), 1);
2835 assert_eq!(items[0].field.name, Some("ParentImage".to_string()));
2837 } else {
2838 panic!("Expected AllOf");
2839 }
2840 }
2841
2842 #[test]
2847 fn test_field_name_mapping_with_cond_not() {
2848 let mut rule = make_test_rule();
2849 let mut state = PipelineState::default();
2850
2851 let field_conds = vec![FieldNameCondition::IncludeFields {
2853 matcher: super::super::conditions::FieldMatcher::Plain(vec!["CommandLine".to_string()]),
2854 }];
2855
2856 let mut mapping = HashMap::new();
2857 mapping.insert("CommandLine".to_string(), vec!["cmd".to_string()]);
2858 mapping.insert("ParentImage".to_string(), vec!["parent".to_string()]);
2859
2860 let t = Transformation::FieldNameMapping { mapping };
2861 t.apply(&mut rule, &mut state, &[], &field_conds, true)
2863 .unwrap();
2864
2865 let det = &rule.detection.named["selection"];
2866 if let Detection::AllOf(items) = det {
2867 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2869 assert_eq!(items[1].field.name, Some("parent".to_string()));
2871 } else {
2872 panic!("Expected AllOf");
2873 }
2874 }
2875
2876 #[test]
2881 fn test_field_name_mapping_empty() {
2882 let mut rule = make_test_rule();
2883 let mut state = PipelineState::default();
2884 let t = Transformation::FieldNameMapping {
2885 mapping: HashMap::new(),
2886 };
2887 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2888
2889 let det = &rule.detection.named["selection"];
2891 if let Detection::AllOf(items) = det {
2892 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2893 assert_eq!(items[1].field.name, Some("ParentImage".to_string()));
2894 } else {
2895 panic!("Expected AllOf");
2896 }
2897 }
2898
2899 #[test]
2900 fn test_field_name_prefix_mapping_empty() {
2901 let mut rule = make_test_rule();
2902 let mut state = PipelineState::default();
2903 let t = Transformation::FieldNamePrefixMapping {
2904 mapping: HashMap::new(),
2905 };
2906 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2907
2908 let det = &rule.detection.named["selection"];
2909 if let Detection::AllOf(items) = det {
2910 assert_eq!(items[0].field.name, Some("CommandLine".to_string()));
2911 } else {
2912 panic!("Expected AllOf");
2913 }
2914 }
2915
2916 #[test]
2917 fn test_map_string_empty_mapping() {
2918 let mut rule = make_test_rule();
2919 let mut state = PipelineState::default();
2920 let t = Transformation::MapString {
2921 mapping: HashMap::new(),
2922 };
2923 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2924
2925 let det = &rule.detection.named["selection"];
2926 if let Detection::AllOf(items) = det
2927 && let SigmaValue::String(s) = &items[0].values[0]
2928 {
2929 assert_eq!(s.original, "whoami");
2930 }
2931 }
2932
2933 #[test]
2934 fn test_hashes_fields_empty_algos() {
2935 let mut named = HashMap::new();
2937 named.insert(
2938 "selection".to_string(),
2939 Detection::AllOf(vec![DetectionItem {
2940 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2941 values: vec![SigmaValue::String(SigmaString::new(
2942 "SHA256=abc123,IMPHASH=def456",
2943 ))],
2944 }]),
2945 );
2946
2947 let mut rule = make_test_rule();
2948 rule.detection.named = named;
2949
2950 let mut state = PipelineState::default();
2951 let t = Transformation::HashesFields {
2952 valid_hash_algos: vec![], field_prefix: "File".to_string(),
2954 drop_algo_prefix: false,
2955 };
2956 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2957
2958 let det = &rule.detection.named["selection"];
2959 if let Detection::AllOf(items) = det {
2960 assert_eq!(items.len(), 2);
2961 assert_eq!(items[0].field.name, Some("FileSHA256".to_string()));
2962 assert_eq!(items[1].field.name, Some("FileIMPHASH".to_string()));
2963 } else {
2964 panic!("Expected AllOf");
2965 }
2966 }
2967
2968 #[test]
2969 fn test_hashes_fields_drop_algo_prefix() {
2970 let mut named = HashMap::new();
2971 named.insert(
2972 "selection".to_string(),
2973 Detection::AllOf(vec![DetectionItem {
2974 field: FieldSpec::new(Some("Hashes".to_string()), vec![]),
2975 values: vec![SigmaValue::String(SigmaString::new("MD5=abc123"))],
2976 }]),
2977 );
2978
2979 let mut rule = make_test_rule();
2980 rule.detection.named = named;
2981 let mut state = PipelineState::default();
2982
2983 let t = Transformation::HashesFields {
2984 valid_hash_algos: vec!["MD5".to_string()],
2985 field_prefix: "Hash".to_string(),
2986 drop_algo_prefix: true,
2987 };
2988 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
2989
2990 let det = &rule.detection.named["selection"];
2991 if let Detection::AllOf(items) = det {
2992 assert_eq!(items.len(), 1);
2993 assert_eq!(items[0].field.name, Some("Hash".to_string()));
2995 } else {
2996 panic!("Expected AllOf");
2997 }
2998 }
2999
3000 #[test]
3005 fn test_replace_string_invalid_regex() {
3006 let mut rule = make_test_rule();
3007 let mut state = PipelineState::default();
3008 let t = Transformation::ReplaceString {
3009 regex: r"[invalid".to_string(), replacement: "x".to_string(),
3011 skip_special: false,
3012 };
3013 let result = t.apply(&mut rule, &mut state, &[], &[], false);
3014 assert!(result.is_err());
3015 let err = result.unwrap_err().to_string();
3016 assert!(
3017 err.contains("bad regex"),
3018 "error should mention regex: {err}"
3019 );
3020 }
3021
3022 #[test]
3027 fn test_case_transformation_with_negated_match_string() {
3028 let mut rule = make_test_rule();
3029 let mut state = PipelineState::default();
3030
3031 let det_conds = vec![DetectionItemCondition::MatchString {
3033 regex: regex::Regex::new("whoami").unwrap(),
3034 negate: true,
3035 }];
3036
3037 let t = Transformation::CaseTransformation {
3038 case_type: "upper".to_string(),
3039 };
3040 t.apply(&mut rule, &mut state, &det_conds, &[], false)
3041 .unwrap();
3042
3043 let det = &rule.detection.named["selection"];
3044 if let Detection::AllOf(items) = det {
3045 if let SigmaValue::String(s) = &items[0].values[0] {
3047 assert_eq!(s.original, "whoami");
3048 }
3049 if let SigmaValue::String(s) = &items[1].values[0] {
3051 assert_eq!(s.original, "\\CMD.EXE");
3052 }
3053 } else {
3054 panic!("Expected AllOf");
3055 }
3056 }
3057
3058 #[test]
3063 fn test_multi_transformation_chaining_pipeline() {
3064 use crate::pipeline::parse_pipeline;
3065
3066 let yaml = r#"
3067name: Multi-step Pipeline
3068transformations:
3069 - id: step1_map
3070 type: field_name_mapping
3071 mapping:
3072 CommandLine: process.command_line
3073 ParentImage: process.parent.executable
3074 - id: step2_prefix
3075 type: field_name_prefix
3076 prefix: "winlog."
3077 rule_conditions:
3078 - type: logsource
3079 product: windows
3080 - id: step3_case
3081 type: case_transformation
3082 case_type: upper
3083 field_name_conditions:
3084 - type: include_fields
3085 fields:
3086 - winlog.process.command_line
3087 - id: step4_attr
3088 type: set_custom_attribute
3089 attribute: rsigma.processed
3090 value: "true"
3091"#;
3092 let pipeline = parse_pipeline(yaml).unwrap();
3093
3094 let mut rule = make_test_rule(); let mut state = PipelineState::new(pipeline.vars.clone());
3096 pipeline.apply(&mut rule, &mut state).unwrap();
3097
3098 let det = &rule.detection.named["selection"];
3099 if let Detection::AllOf(items) = det {
3100 assert_eq!(
3103 items[0].field.name,
3104 Some("winlog.process.command_line".to_string())
3105 );
3106 if let SigmaValue::String(s) = &items[0].values[0] {
3108 assert_eq!(s.original, "WHOAMI");
3109 }
3110
3111 assert_eq!(
3113 items[1].field.name,
3114 Some("winlog.process.parent.executable".to_string())
3115 );
3116 if let SigmaValue::String(s) = &items[1].values[0] {
3118 assert_eq!(s.original, "\\cmd.exe");
3119 }
3120 } else {
3121 panic!("Expected AllOf");
3122 }
3123
3124 assert_eq!(
3126 rule.custom_attributes
3127 .get("rsigma.processed")
3128 .and_then(|v| v.as_str()),
3129 Some("true")
3130 );
3131
3132 assert!(state.was_applied("step1_map"));
3134 assert!(state.was_applied("step2_prefix"));
3135 assert!(state.was_applied("step3_case"));
3136 assert!(state.was_applied("step4_attr"));
3137 }
3138
3139 #[test]
3144 fn test_map_string_one_to_many() {
3145 let mut rule = make_test_rule();
3146 let mut state = PipelineState::default();
3147 let mut mapping = HashMap::new();
3148 mapping.insert(
3149 "whoami".to_string(),
3150 vec!["who".to_string(), "am_i".to_string(), "test".to_string()],
3151 );
3152 let t = Transformation::MapString { mapping };
3153 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3154
3155 let det = &rule.detection.named["selection"];
3156 if let Detection::AllOf(items) = det {
3157 assert_eq!(items[0].values.len(), 3);
3159 if let SigmaValue::String(s) = &items[0].values[0] {
3160 assert_eq!(s.original, "who");
3161 }
3162 if let SigmaValue::String(s) = &items[0].values[1] {
3163 assert_eq!(s.original, "am_i");
3164 }
3165 if let SigmaValue::String(s) = &items[0].values[2] {
3166 assert_eq!(s.original, "test");
3167 }
3168 } else {
3169 panic!("Expected AllOf");
3170 }
3171 }
3172
3173 #[test]
3174 fn test_map_string_one_to_many_mixed() {
3175 let yaml = r#"
3177title: Test Rule
3178logsource:
3179 product: windows
3180detection:
3181 selection:
3182 CommandLine:
3183 - whoami
3184 - ipconfig
3185 condition: selection
3186level: medium
3187"#;
3188 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
3189 let mut rule = collection.rules[0].clone();
3190 let mut state = PipelineState::default();
3191 let mut mapping = HashMap::new();
3192 mapping.insert(
3193 "whoami".to_string(),
3194 vec!["who".to_string(), "am_i".to_string()],
3195 );
3196 let t = Transformation::MapString { mapping };
3198 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3199
3200 let det = &rule.detection.named["selection"];
3201 if let Detection::AllOf(items) = det {
3202 assert_eq!(items[0].values.len(), 3);
3204 if let SigmaValue::String(s) = &items[0].values[0] {
3205 assert_eq!(s.original, "who");
3206 }
3207 if let SigmaValue::String(s) = &items[0].values[1] {
3208 assert_eq!(s.original, "am_i");
3209 }
3210 if let SigmaValue::String(s) = &items[0].values[2] {
3211 assert_eq!(s.original, "ipconfig");
3212 }
3213 } else {
3214 panic!("Expected AllOf");
3215 }
3216 }
3217
3218 #[test]
3223 fn test_replace_string_skip_special_preserves_wildcards() {
3224 let yaml = r#"
3226title: Test Rule
3227logsource:
3228 product: windows
3229detection:
3230 selection:
3231 CommandLine: '*whoami*'
3232 condition: selection
3233level: medium
3234"#;
3235 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
3236 let mut rule = collection.rules[0].clone();
3237 let mut state = PipelineState::default();
3238 let t = Transformation::ReplaceString {
3239 regex: r"whoami".to_string(),
3240 replacement: "REPLACED".to_string(),
3241 skip_special: true,
3242 };
3243 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3244
3245 let det = &rule.detection.named["selection"];
3246 if let Detection::AllOf(items) = det {
3247 let s = match &items[0].values[0] {
3248 SigmaValue::String(s) => s,
3249 _ => panic!("Expected String"),
3250 };
3251 assert!(s.contains_wildcards(), "Wildcards should be preserved");
3253 assert!(
3254 s.original.contains("REPLACED"),
3255 "Plain part should be replaced, got: {}",
3256 s.original
3257 );
3258 assert!(
3259 !s.original.contains("whoami"),
3260 "Original text should be gone"
3261 );
3262 } else {
3263 panic!("Expected AllOf");
3264 }
3265 }
3266
3267 #[test]
3268 fn test_replace_string_skip_special_false_replaces_whole() {
3269 let yaml = r#"
3271title: Test Rule
3272logsource:
3273 product: windows
3274detection:
3275 selection:
3276 CommandLine: '*whoami*'
3277 condition: selection
3278level: medium
3279"#;
3280 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
3281 let mut rule = collection.rules[0].clone();
3282 let mut state = PipelineState::default();
3283 let t = Transformation::ReplaceString {
3284 regex: r"\*".to_string(),
3285 replacement: "STAR".to_string(),
3286 skip_special: false,
3287 };
3288 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3289
3290 let det = &rule.detection.named["selection"];
3291 if let Detection::AllOf(items) = det {
3292 let s = match &items[0].values[0] {
3293 SigmaValue::String(s) => s,
3294 _ => panic!("Expected String"),
3295 };
3296 assert!(
3298 s.original.contains("STAR"),
3299 "Wildcards in original should be replaced as text, got: {}",
3300 s.original
3301 );
3302 } else {
3303 panic!("Expected AllOf");
3304 }
3305 }
3306
3307 #[test]
3308 fn test_replace_string_skip_special_plain_string() {
3309 let mut rule = make_test_rule();
3311 let mut state = PipelineState::default();
3312 let t = Transformation::ReplaceString {
3313 regex: r"whoami".to_string(),
3314 replacement: "REPLACED".to_string(),
3315 skip_special: true,
3316 };
3317 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3318
3319 let det = &rule.detection.named["selection"];
3320 if let Detection::AllOf(items) = det
3321 && let SigmaValue::String(s) = &items[0].values[0]
3322 {
3323 assert_eq!(s.original, "REPLACED");
3324 }
3325 }
3326
3327 #[test]
3332 fn test_case_transformation_snake_case() {
3333 let yaml = r#"
3334title: Test Rule
3335logsource:
3336 product: windows
3337detection:
3338 selection:
3339 CommandLine: CommandAndControl
3340 condition: selection
3341level: medium
3342"#;
3343 let collection = rsigma_parser::parse_sigma_yaml(yaml).unwrap();
3344 let mut rule = collection.rules[0].clone();
3345 let mut state = PipelineState::default();
3346 let t = Transformation::CaseTransformation {
3347 case_type: "snake_case".to_string(),
3348 };
3349 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3350
3351 let det = &rule.detection.named["selection"];
3352 if let Detection::AllOf(items) = det {
3353 if let SigmaValue::String(s) = &items[0].values[0] {
3354 assert_eq!(s.original, "command_and_control");
3355 } else {
3356 panic!("Expected String");
3357 }
3358 } else {
3359 panic!("Expected AllOf");
3360 }
3361 }
3362
3363 #[test]
3364 fn test_case_transformation_snake_case_already_lowercase() {
3365 let mut rule = make_test_rule(); let mut state = PipelineState::default();
3367 let t = Transformation::CaseTransformation {
3368 case_type: "snake_case".to_string(),
3369 };
3370 t.apply(&mut rule, &mut state, &[], &[], false).unwrap();
3371
3372 let det = &rule.detection.named["selection"];
3373 if let Detection::AllOf(items) = det
3374 && let SigmaValue::String(s) = &items[0].values[0]
3375 {
3376 assert_eq!(s.original, "whoami"); }
3378 }
3379}