1use std::collections::HashMap;
8
9use rsigma_eval::pipeline::state::PipelineState;
10use rsigma_parser::*;
11
12use crate::backend::*;
13use crate::condition::convert_condition_expr;
14use crate::convert::{default_convert_detection, default_convert_detection_item};
15use crate::error::{ConvertError, Result};
16use crate::state::{ConversionState, ConvertResult};
17
18pub static TEXT_QUERY_TEST_CONFIG: TextQueryConfig = TextQueryConfig {
23 precedence: (TokenType::NOT, TokenType::AND, TokenType::OR),
24 group_expression: "({expr})",
25 token_separator: " ",
26
27 and_token: "and",
28 or_token: "or",
29 not_token: "not",
30 eq_token: "=",
31
32 not_eq_token: Some("!="),
33 eq_expression: None,
34 not_eq_expression: None,
35 convert_not_as_not_eq: false,
36
37 wildcard_multi: "*",
38 wildcard_single: "?",
39
40 str_quote: "\"",
41 str_quote_pattern: None,
42 str_quote_pattern_negation: false,
43 escape_char: "\\",
44 add_escaped: &[":"],
45 filter_chars: &["&"],
46
47 field_quote: Some("'"),
48 field_quote_pattern: Some(r"^\w+$"),
49 field_quote_pattern_negation: true,
50 field_escape: None,
51 field_escape_pattern: None,
52
53 startswith_expression: Some("{field} startswith {value}"),
54 not_startswith_expression: None,
55 startswith_expression_allow_special: false,
56 endswith_expression: Some("{field} endswith {value}"),
57 not_endswith_expression: None,
58 endswith_expression_allow_special: false,
59 contains_expression: Some("{field} contains {value}"),
60 not_contains_expression: None,
61 contains_expression_allow_special: false,
62 wildcard_match_expression: Some("{field} match {value}"),
63
64 case_sensitive_match_expression: Some("{field} casematch {value}"),
65 case_sensitive_startswith_expression: Some("{field} startswith_cased {value}"),
66 case_sensitive_endswith_expression: Some("{field} endswith_cased {value}"),
67 case_sensitive_contains_expression: Some("{field} contains_cased {value}"),
68
69 re_expression: Some("{field}=/{regex}/"),
70 not_re_expression: None,
71 re_escape_char: Some("\\"),
72 re_escape: &["/"],
73 re_escape_escape_char: None,
74
75 cidr_expression: Some("cidrmatch(\"{value}\", {field})"),
76 not_cidr_expression: None,
77
78 field_null_expression: "{field} is null",
79 field_exists_expression: Some("exists({field})"),
80 field_not_exists_expression: Some("notexists({field})"),
81
82 compare_op_expression: Some("{field}{op}{value}"),
83 compare_ops: &[
84 ("lt", "<"),
85 ("lte", "<="),
86 ("gt", ">"),
87 ("gte", ">="),
88 ("neq", "!="),
89 ],
90
91 convert_or_as_in: true,
92 convert_and_as_in: true,
93 in_expressions_allow_wildcards: true,
94 field_in_list_expression: Some("{field} {op} ({list})"),
95 or_in_operator: Some("in"),
96 and_in_operator: Some("contains-all"),
97 list_separator: ", ",
98
99 unbound_value_str_expression: Some("_={value}"),
100 unbound_value_num_expression: Some("_={value}"),
101 unbound_value_re_expression: Some("_=/{value}/"),
102
103 field_eq_field_expression: Some("{field1}=fieldref({field2})"),
104 field_eq_field_escaping_quoting: true,
105
106 deferred_start: Some(" | "),
107 deferred_separator: Some(" | "),
108 deferred_only_query: "*",
109
110 bool_true: "1",
111 bool_false: "0",
112 query_expression: "{query}",
113 state_defaults: &[],
114};
115
116pub struct TextQueryTestBackend {
121 pub config: &'static TextQueryConfig,
122}
123
124impl TextQueryTestBackend {
125 pub fn new() -> Self {
126 Self {
127 config: &TEXT_QUERY_TEST_CONFIG,
128 }
129 }
130}
131
132impl Default for TextQueryTestBackend {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138impl Backend for TextQueryTestBackend {
139 fn name(&self) -> &str {
140 "test"
141 }
142
143 fn formats(&self) -> &[(&str, &str)] {
144 &[
145 ("default", "plain query list"),
146 ("test", "wrapped query [ {query} ]"),
147 ("state", "index={state.index} ({query})"),
148 ("str", "newline-joined queries"),
149 ]
150 }
151
152 fn requires_pipeline(&self) -> bool {
153 false
154 }
155
156 fn convert_rule(
159 &self,
160 rule: &SigmaRule,
161 output_format: &str,
162 pipeline_state: &PipelineState,
163 ) -> Result<Vec<String>> {
164 let mut queries = Vec::new();
165
166 for (idx, cond_expr) in rule.detection.conditions.iter().enumerate() {
167 let mut state = ConversionState::new(pipeline_state.state.clone());
168 let query = self.convert_condition(cond_expr, &rule.detection.named, &mut state)?;
169 let finished = self.finish_query(rule, query, &state)?;
170 let finalized = self.finalize_query(rule, finished, idx, &state, output_format)?;
171 queries.push(finalized);
172 }
173
174 Ok(queries)
175 }
176
177 fn convert_condition(
180 &self,
181 expr: &ConditionExpr,
182 detections: &HashMap<String, Detection>,
183 state: &mut ConversionState,
184 ) -> Result<String> {
185 convert_condition_expr(self, expr, detections, state)
186 }
187
188 fn convert_condition_and(&self, exprs: &[String]) -> Result<String> {
189 Ok(text_convert_condition_and(self.config, exprs))
190 }
191
192 fn convert_condition_or(&self, exprs: &[String]) -> Result<String> {
193 Ok(text_convert_condition_or(self.config, exprs))
194 }
195
196 fn convert_condition_not(&self, expr: &str) -> Result<String> {
197 Ok(text_convert_condition_not(self.config, expr))
198 }
199
200 fn convert_detection(&self, det: &Detection, state: &mut ConversionState) -> Result<String> {
203 default_convert_detection(self, det, state)
204 }
205
206 fn convert_detection_item(
207 &self,
208 item: &DetectionItem,
209 state: &mut ConversionState,
210 ) -> Result<String> {
211 default_convert_detection_item(self, item, state)
212 }
213
214 fn escape_and_quote_field(&self, field: &str) -> String {
217 text_escape_and_quote_field(self.config, field)
218 }
219
220 fn convert_value_str(&self, value: &SigmaString, _state: &ConversionState) -> String {
221 text_convert_value_str(self.config, value)
222 }
223
224 fn convert_value_re(&self, regex: &str, _state: &ConversionState) -> String {
225 text_convert_value_re(self.config, regex)
226 }
227
228 fn convert_field_eq_str(
231 &self,
232 field: &str,
233 value: &SigmaString,
234 modifiers: &[Modifier],
235 state: &mut ConversionState,
236 ) -> Result<ConvertResult> {
237 text_convert_field_eq_str(self.config, field, value, modifiers, state)
238 }
239
240 fn convert_field_eq_str_case_sensitive(
241 &self,
242 field: &str,
243 value: &SigmaString,
244 modifiers: &[Modifier],
245 state: &mut ConversionState,
246 ) -> Result<ConvertResult> {
247 let mut mods = modifiers.to_vec();
248 if !mods.contains(&Modifier::Cased) {
249 mods.push(Modifier::Cased);
250 }
251 text_convert_field_eq_str(self.config, field, value, &mods, state)
252 }
253
254 fn convert_field_eq_num(
255 &self,
256 field: &str,
257 value: f64,
258 _state: &mut ConversionState,
259 ) -> Result<String> {
260 let f = text_escape_and_quote_field(self.config, field);
261 if value.fract() == 0.0 {
262 Ok(format!("{f}={}", value as i64))
263 } else {
264 Ok(format!("{f}={value}"))
265 }
266 }
267
268 fn convert_field_eq_bool(
269 &self,
270 field: &str,
271 value: bool,
272 _state: &mut ConversionState,
273 ) -> Result<String> {
274 let f = text_escape_and_quote_field(self.config, field);
275 let v = if value {
276 self.config.bool_true
277 } else {
278 self.config.bool_false
279 };
280 Ok(format!("{f}={v}"))
281 }
282
283 fn convert_field_eq_null(&self, field: &str, _state: &mut ConversionState) -> Result<String> {
284 let f = text_escape_and_quote_field(self.config, field);
285 Ok(self.config.field_null_expression.replace("{field}", &f))
286 }
287
288 fn convert_field_eq_re(
289 &self,
290 field: &str,
291 pattern: &str,
292 _flags: &[Modifier],
293 _state: &mut ConversionState,
294 ) -> Result<ConvertResult> {
295 let f = text_escape_and_quote_field(self.config, field);
296 let re_val = text_convert_value_re(self.config, pattern);
297 let expr = self
298 .config
299 .re_expression
300 .ok_or_else(|| ConvertError::UnsupportedModifier("regex".into()))?;
301 Ok(ConvertResult::Query(
302 expr.replace("{field}", &f).replace("{regex}", &re_val),
303 ))
304 }
305
306 fn convert_field_eq_cidr(
307 &self,
308 field: &str,
309 cidr: &str,
310 _state: &mut ConversionState,
311 ) -> Result<ConvertResult> {
312 let f = text_escape_and_quote_field(self.config, field);
313 let expr = self
314 .config
315 .cidr_expression
316 .ok_or_else(|| ConvertError::UnsupportedModifier("cidr".into()))?;
317 Ok(ConvertResult::Query(
318 expr.replace("{field}", &f).replace("{value}", cidr),
319 ))
320 }
321
322 fn convert_field_compare(
323 &self,
324 field: &str,
325 op: &Modifier,
326 value: f64,
327 _state: &mut ConversionState,
328 ) -> Result<String> {
329 let f = text_escape_and_quote_field(self.config, field);
330 let op_name = match op {
331 Modifier::Lt => "lt",
332 Modifier::Lte => "lte",
333 Modifier::Gt => "gt",
334 Modifier::Gte => "gte",
335 _ => {
336 return Err(ConvertError::UnsupportedModifier(format!(
337 "compare op {:?}",
338 op
339 )));
340 }
341 };
342 let op_token = self
343 .config
344 .compare_ops
345 .iter()
346 .find(|(name, _)| *name == op_name)
347 .map(|(_, token)| *token)
348 .ok_or_else(|| ConvertError::UnsupportedModifier(op_name.into()))?;
349
350 let expr = self
351 .config
352 .compare_op_expression
353 .ok_or_else(|| ConvertError::UnsupportedModifier("compare".into()))?;
354
355 let val_str = if value.fract() == 0.0 {
356 (value as i64).to_string()
357 } else {
358 value.to_string()
359 };
360 Ok(expr
361 .replace("{field}", &f)
362 .replace("{op}", op_token)
363 .replace("{value}", &val_str))
364 }
365
366 fn convert_field_exists(
367 &self,
368 field: &str,
369 exists: bool,
370 _state: &mut ConversionState,
371 ) -> Result<String> {
372 let f = text_escape_and_quote_field(self.config, field);
373 if exists {
374 let expr = self
375 .config
376 .field_exists_expression
377 .ok_or_else(|| ConvertError::UnsupportedModifier("exists".into()))?;
378 Ok(expr.replace("{field}", &f))
379 } else {
380 let expr = self
381 .config
382 .field_not_exists_expression
383 .ok_or_else(|| ConvertError::UnsupportedModifier("not exists".into()))?;
384 Ok(expr.replace("{field}", &f))
385 }
386 }
387
388 fn convert_field_eq_query_expr(
389 &self,
390 field: &str,
391 expr: &str,
392 _id: &str,
393 _state: &mut ConversionState,
394 ) -> Result<String> {
395 let f = text_escape_and_quote_field(self.config, field);
396 Ok(format!("{f}={expr}"))
397 }
398
399 fn convert_field_ref(
400 &self,
401 field1: &str,
402 field2: &str,
403 _state: &mut ConversionState,
404 ) -> Result<ConvertResult> {
405 let expr = self
406 .config
407 .field_eq_field_expression
408 .ok_or_else(|| ConvertError::UnsupportedModifier("fieldref".into()))?;
409 let f1 = text_escape_and_quote_field(self.config, field1);
410 let f2 = if self.config.field_eq_field_escaping_quoting {
411 text_escape_and_quote_field(self.config, field2)
412 } else {
413 field2.to_string()
414 };
415 Ok(ConvertResult::Query(
416 expr.replace("{field1}", &f1).replace("{field2}", &f2),
417 ))
418 }
419
420 fn convert_keyword(&self, value: &SigmaValue, _state: &mut ConversionState) -> Result<String> {
421 match value {
422 SigmaValue::String(s) => {
423 let v = text_convert_value_str(self.config, s);
424 let expr = self
425 .config
426 .unbound_value_str_expression
427 .ok_or(ConvertError::UnsupportedKeyword)?;
428 Ok(expr.replace("{value}", &v))
429 }
430 SigmaValue::Integer(n) => {
431 let expr = self
432 .config
433 .unbound_value_num_expression
434 .ok_or(ConvertError::UnsupportedKeyword)?;
435 Ok(expr.replace("{value}", &n.to_string()))
436 }
437 SigmaValue::Float(f) => {
438 let expr = self
439 .config
440 .unbound_value_num_expression
441 .ok_or(ConvertError::UnsupportedKeyword)?;
442 Ok(expr.replace("{value}", &f.to_string()))
443 }
444 _ => Err(ConvertError::UnsupportedKeyword),
445 }
446 }
447
448 fn convert_condition_as_in_expression(
449 &self,
450 field: &str,
451 values: &[&SigmaValue],
452 is_or: bool,
453 _state: &mut ConversionState,
454 ) -> Result<String> {
455 let f = text_escape_and_quote_field(self.config, field);
456 let expr = self
457 .config
458 .field_in_list_expression
459 .ok_or_else(|| ConvertError::UnsupportedModifier("in-list".into()))?;
460 let op = if is_or {
461 self.config
462 .or_in_operator
463 .ok_or_else(|| ConvertError::UnsupportedModifier("or-in".into()))?
464 } else {
465 self.config
466 .and_in_operator
467 .ok_or_else(|| ConvertError::UnsupportedModifier("and-in".into()))?
468 };
469
470 let items: Vec<String> = values
471 .iter()
472 .map(|v| match v {
473 SigmaValue::String(s) => text_convert_value_str(self.config, s),
474 SigmaValue::Integer(n) => n.to_string(),
475 SigmaValue::Float(f) => f.to_string(),
476 _ => String::new(),
477 })
478 .collect();
479
480 let list = items.join(self.config.list_separator);
481 Ok(expr
482 .replace("{field}", &f)
483 .replace("{op}", op)
484 .replace("{list}", &list))
485 }
486
487 fn finish_query(
490 &self,
491 rule: &SigmaRule,
492 query: String,
493 state: &ConversionState,
494 ) -> Result<String> {
495 Ok(text_finish_query(self.config, &query, state, rule))
496 }
497
498 fn finalize_query(
499 &self,
500 _rule: &SigmaRule,
501 query: String,
502 _index: usize,
503 state: &ConversionState,
504 output_format: &str,
505 ) -> Result<String> {
506 match output_format {
507 "default" => Ok(query),
508 "test" => Ok(format!("[ {query} ]")),
509 "state" => {
510 let index = state.get_state_str("index").unwrap_or("default_index");
511 Ok(format!("index={index} ({query})"))
512 }
513 "str" => Ok(query),
514 other => Err(ConvertError::RuleConversion(format!(
515 "unknown output format: {other}"
516 ))),
517 }
518 }
519
520 fn finalize_output(&self, queries: Vec<String>, output_format: &str) -> Result<String> {
521 match output_format {
522 "str" => Ok(queries.join("\n")),
523 _ => Ok(queries.join("\n")),
524 }
525 }
526}
527
528pub struct MandatoryPipelineTestBackend(TextQueryTestBackend);
534
535impl MandatoryPipelineTestBackend {
536 pub fn new() -> Self {
537 Self(TextQueryTestBackend::new())
538 }
539}
540
541impl Default for MandatoryPipelineTestBackend {
542 fn default() -> Self {
543 Self::new()
544 }
545}
546
547impl Backend for MandatoryPipelineTestBackend {
548 fn name(&self) -> &str {
549 "test_mandatory_pipeline"
550 }
551
552 fn formats(&self) -> &[(&str, &str)] {
553 self.0.formats()
554 }
555
556 fn requires_pipeline(&self) -> bool {
557 true
558 }
559
560 fn convert_rule(
561 &self,
562 rule: &SigmaRule,
563 output_format: &str,
564 pipeline_state: &PipelineState,
565 ) -> Result<Vec<String>> {
566 self.0.convert_rule(rule, output_format, pipeline_state)
567 }
568
569 fn convert_condition(
570 &self,
571 expr: &ConditionExpr,
572 detections: &HashMap<String, Detection>,
573 state: &mut ConversionState,
574 ) -> Result<String> {
575 self.0.convert_condition(expr, detections, state)
576 }
577
578 fn convert_condition_and(&self, exprs: &[String]) -> Result<String> {
579 self.0.convert_condition_and(exprs)
580 }
581
582 fn convert_condition_or(&self, exprs: &[String]) -> Result<String> {
583 self.0.convert_condition_or(exprs)
584 }
585
586 fn convert_condition_not(&self, expr: &str) -> Result<String> {
587 self.0.convert_condition_not(expr)
588 }
589
590 fn convert_detection(&self, det: &Detection, state: &mut ConversionState) -> Result<String> {
591 self.0.convert_detection(det, state)
592 }
593
594 fn convert_detection_item(
595 &self,
596 item: &DetectionItem,
597 state: &mut ConversionState,
598 ) -> Result<String> {
599 self.0.convert_detection_item(item, state)
600 }
601
602 fn escape_and_quote_field(&self, field: &str) -> String {
603 self.0.escape_and_quote_field(field)
604 }
605
606 fn convert_value_str(&self, value: &SigmaString, state: &ConversionState) -> String {
607 self.0.convert_value_str(value, state)
608 }
609
610 fn convert_value_re(&self, regex: &str, state: &ConversionState) -> String {
611 self.0.convert_value_re(regex, state)
612 }
613
614 fn convert_field_eq_str(
615 &self,
616 field: &str,
617 value: &SigmaString,
618 modifiers: &[Modifier],
619 state: &mut ConversionState,
620 ) -> Result<ConvertResult> {
621 self.0.convert_field_eq_str(field, value, modifiers, state)
622 }
623
624 fn convert_field_eq_str_case_sensitive(
625 &self,
626 field: &str,
627 value: &SigmaString,
628 modifiers: &[Modifier],
629 state: &mut ConversionState,
630 ) -> Result<ConvertResult> {
631 self.0
632 .convert_field_eq_str_case_sensitive(field, value, modifiers, state)
633 }
634
635 fn convert_field_eq_num(
636 &self,
637 field: &str,
638 value: f64,
639 state: &mut ConversionState,
640 ) -> Result<String> {
641 self.0.convert_field_eq_num(field, value, state)
642 }
643
644 fn convert_field_eq_bool(
645 &self,
646 field: &str,
647 value: bool,
648 state: &mut ConversionState,
649 ) -> Result<String> {
650 self.0.convert_field_eq_bool(field, value, state)
651 }
652
653 fn convert_field_eq_null(&self, field: &str, state: &mut ConversionState) -> Result<String> {
654 self.0.convert_field_eq_null(field, state)
655 }
656
657 fn convert_field_eq_re(
658 &self,
659 field: &str,
660 pattern: &str,
661 flags: &[Modifier],
662 state: &mut ConversionState,
663 ) -> Result<ConvertResult> {
664 self.0.convert_field_eq_re(field, pattern, flags, state)
665 }
666
667 fn convert_field_eq_cidr(
668 &self,
669 field: &str,
670 cidr: &str,
671 state: &mut ConversionState,
672 ) -> Result<ConvertResult> {
673 self.0.convert_field_eq_cidr(field, cidr, state)
674 }
675
676 fn convert_field_compare(
677 &self,
678 field: &str,
679 op: &Modifier,
680 value: f64,
681 state: &mut ConversionState,
682 ) -> Result<String> {
683 self.0.convert_field_compare(field, op, value, state)
684 }
685
686 fn convert_field_exists(
687 &self,
688 field: &str,
689 exists: bool,
690 state: &mut ConversionState,
691 ) -> Result<String> {
692 self.0.convert_field_exists(field, exists, state)
693 }
694
695 fn convert_field_eq_query_expr(
696 &self,
697 field: &str,
698 expr: &str,
699 id: &str,
700 state: &mut ConversionState,
701 ) -> Result<String> {
702 self.0.convert_field_eq_query_expr(field, expr, id, state)
703 }
704
705 fn convert_field_ref(
706 &self,
707 field1: &str,
708 field2: &str,
709 state: &mut ConversionState,
710 ) -> Result<ConvertResult> {
711 self.0.convert_field_ref(field1, field2, state)
712 }
713
714 fn convert_keyword(&self, value: &SigmaValue, state: &mut ConversionState) -> Result<String> {
715 self.0.convert_keyword(value, state)
716 }
717
718 fn finish_query(
719 &self,
720 rule: &SigmaRule,
721 query: String,
722 state: &ConversionState,
723 ) -> Result<String> {
724 self.0.finish_query(rule, query, state)
725 }
726
727 fn finalize_query(
728 &self,
729 rule: &SigmaRule,
730 query: String,
731 index: usize,
732 state: &ConversionState,
733 output_format: &str,
734 ) -> Result<String> {
735 self.0
736 .finalize_query(rule, query, index, state, output_format)
737 }
738
739 fn finalize_output(&self, queries: Vec<String>, output_format: &str) -> Result<String> {
740 self.0.finalize_output(queries, output_format)
741 }
742}
743
744#[cfg(test)]
749mod tests {
750 use super::*;
751 use rsigma_parser::parse_sigma_yaml;
752
753 fn convert_rule_yaml(yaml: &str) -> Vec<String> {
754 let collection = parse_sigma_yaml(yaml).unwrap();
755 let backend = TextQueryTestBackend::new();
756 let mut results = Vec::new();
757 for rule in &collection.rules {
758 let queries = backend
759 .convert_rule(rule, "default", &PipelineState::default())
760 .unwrap();
761 results.extend(queries);
762 }
763 results
764 }
765
766 #[test]
767 fn test_simple_eq() {
768 let queries = convert_rule_yaml(
769 r#"
770title: Test
771logsource:
772 category: test
773detection:
774 selection:
775 CommandLine: whoami
776 condition: selection
777"#,
778 );
779 assert_eq!(queries, vec!["CommandLine=\"whoami\""]);
780 }
781
782 #[test]
783 fn test_and_condition() {
784 let queries = convert_rule_yaml(
785 r#"
786title: Test
787logsource:
788 category: test
789detection:
790 sel1:
791 FieldA: val1
792 sel2:
793 FieldB: val2
794 condition: sel1 and sel2
795"#,
796 );
797 assert_eq!(queries, vec!["FieldA=\"val1\" and FieldB=\"val2\""]);
798 }
799
800 #[test]
801 fn test_or_condition() {
802 let queries = convert_rule_yaml(
803 r#"
804title: Test
805logsource:
806 category: test
807detection:
808 sel1:
809 FieldA: val1
810 sel2:
811 FieldB: val2
812 condition: sel1 or sel2
813"#,
814 );
815 assert_eq!(queries, vec!["FieldA=\"val1\" or FieldB=\"val2\""]);
816 }
817
818 #[test]
819 fn test_not_condition() {
820 let queries = convert_rule_yaml(
821 r#"
822title: Test
823logsource:
824 category: test
825detection:
826 selection:
827 FieldA: val1
828 filter:
829 FieldB: val2
830 condition: selection and not filter
831"#,
832 );
833 assert_eq!(queries, vec!["FieldA=\"val1\" and not FieldB=\"val2\""]);
834 }
835
836 #[test]
837 fn test_contains_modifier() {
838 let queries = convert_rule_yaml(
839 r#"
840title: Test
841logsource:
842 category: test
843detection:
844 selection:
845 CommandLine|contains: whoami
846 condition: selection
847"#,
848 );
849 assert_eq!(queries, vec!["CommandLine contains \"whoami\""]);
850 }
851
852 #[test]
853 fn test_startswith_modifier() {
854 let queries = convert_rule_yaml(
855 r#"
856title: Test
857logsource:
858 category: test
859detection:
860 selection:
861 CommandLine|startswith: cmd
862 condition: selection
863"#,
864 );
865 assert_eq!(queries, vec!["CommandLine startswith \"cmd\""]);
866 }
867
868 #[test]
869 fn test_endswith_modifier() {
870 let queries = convert_rule_yaml(
871 r#"
872title: Test
873logsource:
874 category: test
875detection:
876 selection:
877 CommandLine|endswith: '.exe'
878 condition: selection
879"#,
880 );
881 assert_eq!(queries, vec!["CommandLine endswith \".exe\""]);
882 }
883
884 #[test]
885 fn test_wildcard_value() {
886 let queries = convert_rule_yaml(
887 r#"
888title: Test
889logsource:
890 category: test
891detection:
892 selection:
893 CommandLine: '*whoami*'
894 condition: selection
895"#,
896 );
897 assert_eq!(queries, vec!["CommandLine match *whoami*"]);
898 }
899
900 #[test]
901 fn test_numeric_value() {
902 let queries = convert_rule_yaml(
903 r#"
904title: Test
905logsource:
906 category: test
907detection:
908 selection:
909 EventID: 4688
910 condition: selection
911"#,
912 );
913 assert_eq!(queries, vec!["EventID=4688"]);
914 }
915
916 #[test]
917 fn test_boolean_value() {
918 let queries = convert_rule_yaml(
919 r#"
920title: Test
921logsource:
922 category: test
923detection:
924 selection:
925 Enabled: true
926 condition: selection
927"#,
928 );
929 assert_eq!(queries, vec!["Enabled=1"]);
930 }
931
932 #[test]
933 fn test_null_value() {
934 let queries = convert_rule_yaml(
935 r#"
936title: Test
937logsource:
938 category: test
939detection:
940 selection:
941 FieldA: null
942 condition: selection
943"#,
944 );
945 assert_eq!(queries, vec!["FieldA is null"]);
946 }
947
948 #[test]
949 fn test_exists_modifier() {
950 let queries = convert_rule_yaml(
951 r#"
952title: Test
953logsource:
954 category: test
955detection:
956 selection:
957 FieldA|exists: true
958 condition: selection
959"#,
960 );
961 assert_eq!(queries, vec!["exists(FieldA)"]);
962 }
963
964 #[test]
965 fn test_not_exists_modifier() {
966 let queries = convert_rule_yaml(
967 r#"
968title: Test
969logsource:
970 category: test
971detection:
972 selection:
973 FieldA|exists: false
974 condition: selection
975"#,
976 );
977 assert_eq!(queries, vec!["notexists(FieldA)"]);
978 }
979
980 #[test]
981 fn test_re_modifier() {
982 let queries = convert_rule_yaml(
983 r#"
984title: Test
985logsource:
986 category: test
987detection:
988 selection:
989 CommandLine|re: '.*whoami.*'
990 condition: selection
991"#,
992 );
993 assert_eq!(queries, vec!["CommandLine=/.*whoami.*/"]);
994 }
995
996 #[test]
997 fn test_cidr_modifier() {
998 let queries = convert_rule_yaml(
999 r#"
1000title: Test
1001logsource:
1002 category: test
1003detection:
1004 selection:
1005 SourceIP|cidr: '10.0.0.0/8'
1006 condition: selection
1007"#,
1008 );
1009 assert_eq!(queries, vec!["cidrmatch(\"10.0.0.0/8\", SourceIP)"]);
1010 }
1011
1012 #[test]
1013 fn test_gte_modifier() {
1014 let queries = convert_rule_yaml(
1015 r#"
1016title: Test
1017logsource:
1018 category: test
1019detection:
1020 selection:
1021 EventCount|gte: 10
1022 condition: selection
1023"#,
1024 );
1025 assert_eq!(queries, vec!["EventCount>=10"]);
1026 }
1027
1028 #[test]
1029 fn test_multiple_values_or() {
1030 let queries = convert_rule_yaml(
1031 r#"
1032title: Test
1033logsource:
1034 category: test
1035detection:
1036 selection:
1037 CommandLine:
1038 - whoami
1039 - ipconfig
1040 condition: selection
1041"#,
1042 );
1043 assert_eq!(
1044 queries,
1045 vec!["CommandLine=\"whoami\" or CommandLine=\"ipconfig\""]
1046 );
1047 }
1048
1049 #[test]
1050 fn test_multiple_values_all() {
1051 let queries = convert_rule_yaml(
1052 r#"
1053title: Test
1054logsource:
1055 category: test
1056detection:
1057 selection:
1058 CommandLine|all:
1059 - whoami
1060 - ipconfig
1061 condition: selection
1062"#,
1063 );
1064 assert_eq!(
1065 queries,
1066 vec!["CommandLine=\"whoami\" and CommandLine=\"ipconfig\""]
1067 );
1068 }
1069
1070 #[test]
1071 fn test_escape_chars() {
1072 let queries = convert_rule_yaml(
1073 r#"
1074title: Test
1075logsource:
1076 category: test
1077detection:
1078 selection:
1079 FieldA: 'value:with&special'
1080 condition: selection
1081"#,
1082 );
1083 assert_eq!(queries, vec!["FieldA=\"value\\:withspecial\""]);
1085 }
1086
1087 #[test]
1088 fn test_output_format_test() {
1089 let collection = parse_sigma_yaml(
1090 r#"
1091title: Test
1092logsource:
1093 category: test
1094detection:
1095 selection:
1096 FieldA: val1
1097 condition: selection
1098"#,
1099 )
1100 .unwrap();
1101 let backend = TextQueryTestBackend::new();
1102 let queries = backend
1103 .convert_rule(&collection.rules[0], "test", &PipelineState::default())
1104 .unwrap();
1105 assert_eq!(queries, vec!["[ FieldA=\"val1\" ]"]);
1106 }
1107
1108 #[test]
1109 fn test_output_format_state() {
1110 let collection = parse_sigma_yaml(
1111 r#"
1112title: Test
1113logsource:
1114 category: test
1115detection:
1116 selection:
1117 FieldA: val1
1118 condition: selection
1119"#,
1120 )
1121 .unwrap();
1122 let backend = TextQueryTestBackend::new();
1123 let mut ps = PipelineState::default();
1124 ps.set_state(
1125 "index".to_string(),
1126 serde_json::Value::String("my_index".into()),
1127 );
1128 let queries = backend
1129 .convert_rule(&collection.rules[0], "state", &ps)
1130 .unwrap();
1131 assert_eq!(queries, vec!["index=my_index (FieldA=\"val1\")"]);
1132 }
1133
1134 #[test]
1135 fn test_mandatory_pipeline_error() {
1136 let collection = parse_sigma_yaml(
1137 r#"
1138title: Test
1139logsource:
1140 category: test
1141detection:
1142 selection:
1143 FieldA: val1
1144 condition: selection
1145"#,
1146 )
1147 .unwrap();
1148 let backend = MandatoryPipelineTestBackend::new();
1149 let result = crate::convert::convert_collection(&backend, &collection, &[], "default");
1150 assert!(matches!(result, Err(ConvertError::PipelineRequired)));
1151 }
1152
1153 #[test]
1154 fn test_multiple_detection_items_and() {
1155 let queries = convert_rule_yaml(
1156 r#"
1157title: Test
1158logsource:
1159 category: test
1160detection:
1161 selection:
1162 FieldA: val1
1163 FieldB: val2
1164 condition: selection
1165"#,
1166 );
1167 assert_eq!(queries, vec!["FieldA=\"val1\" and FieldB=\"val2\""]);
1168 }
1169
1170 #[test]
1171 fn test_keywords() {
1172 let queries = convert_rule_yaml(
1173 r#"
1174title: Test
1175logsource:
1176 category: test
1177detection:
1178 keywords:
1179 - whoami
1180 - ipconfig
1181 condition: keywords
1182"#,
1183 );
1184 assert_eq!(queries, vec!["_=\"whoami\" or _=\"ipconfig\""]);
1185 }
1186
1187 #[test]
1188 fn test_case_sensitive_contains() {
1189 let queries = convert_rule_yaml(
1190 r#"
1191title: Test
1192logsource:
1193 category: test
1194detection:
1195 selection:
1196 CommandLine|contains|cased: Whoami
1197 condition: selection
1198"#,
1199 );
1200 assert_eq!(queries, vec!["CommandLine contains_cased \"Whoami\""]);
1201 }
1202
1203 #[test]
1204 fn test_re_with_slash_escaping() {
1205 let queries = convert_rule_yaml(
1206 r#"
1207title: Test
1208logsource:
1209 category: test
1210detection:
1211 selection:
1212 Path|re: 'C:/Windows/.*'
1213 condition: selection
1214"#,
1215 );
1216 assert_eq!(queries, vec!["Path=/C:\\/Windows\\/.*/"]);
1219 }
1220}