1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use rsigma_eval::pipeline::state::PipelineState;
5use rsigma_ir::{IrDetection, IrDetectionItem, IrPattern, IrPatternPart, IrStrOp};
6use rsigma_parser::*;
7
8use crate::error::{ConvertError, Result};
9use crate::state::{ConversionState, ConvertResult};
10
11static REGEX_CACHE: Mutex<Option<HashMap<&'static str, regex::Regex>>> = Mutex::new(None);
13
14fn get_cached_regex(pattern: &'static str) -> Option<regex::Regex> {
15 let mut guard = REGEX_CACHE.lock().unwrap();
16 let cache = guard.get_or_insert_with(HashMap::new);
17 if let Some(re) = cache.get(pattern) {
18 return Some(re.clone());
19 }
20 match regex::Regex::new(pattern) {
21 Ok(re) => {
22 cache.insert(pattern, re.clone());
23 Some(re)
24 }
25 Err(_) => None,
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
35pub enum TokenType {
36 NOT = 0,
38 AND = 1,
39 OR = 2,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CompareOp {
45 Gt,
46 Gte,
47 Lt,
48 Lte,
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
57pub struct RegexFlags {
58 pub case_insensitive: bool,
59 pub multiline: bool,
60 pub dotall: bool,
61 pub cased: bool,
62}
63
64impl RegexFlags {
65 pub fn inline_prefix(&self) -> String {
69 let flags: String = [
70 (self.case_insensitive, 'i'),
71 (self.multiline, 'm'),
72 (self.dotall, 's'),
73 ]
74 .into_iter()
75 .filter_map(|(set, flag)| set.then_some(flag))
76 .collect();
77 if flags.is_empty() {
78 flags
79 } else {
80 format!("(?{flags})")
81 }
82 }
83}
84
85pub(crate) use rsigma_ir::ir_pattern_to_sigma;
91
92pub(crate) fn reject_nul(backend: &str, value: &str) -> Result<()> {
95 if value.contains('\0') {
96 return Err(ConvertError::UnsupportedValue(format!(
97 "{backend} cannot represent a NUL character in a query value; \
98 combine |wide/|utf16 with |base64 or |base64offset"
99 )));
100 }
101 Ok(())
102}
103
104pub(crate) fn reject_nul_pattern(backend: &str, pattern: &IrPattern) -> Result<()> {
106 pattern.parts.iter().try_for_each(|part| match part {
107 IrPatternPart::Literal(s) => reject_nul(backend, s),
108 _ => Ok(()),
109 })
110}
111
112pub trait Backend: Send + Sync {
124 fn name(&self) -> &str;
125 fn formats(&self) -> &[(&str, &str)];
126
127 fn default_format(&self) -> &str {
128 "default"
129 }
130
131 fn requires_pipeline(&self) -> bool {
132 false
133 }
134
135 fn convert_rule(
138 &self,
139 rule: &SigmaRule,
140 output_format: &str,
141 pipeline_state: &PipelineState,
142 ) -> Result<Vec<String>>;
143
144 fn convert_condition_and(&self, exprs: &[String]) -> Result<String>;
147 fn convert_condition_or(&self, exprs: &[String]) -> Result<String>;
148 fn convert_condition_not(&self, expr: &str) -> Result<String>;
149
150 fn convert_condition_group(
158 &self,
159 expr: &str,
160 outer: TokenType,
161 inner: TokenType,
162 ) -> Result<String> {
163 if outer == TokenType::NOT || inner > outer {
164 Ok(format!("({expr})"))
165 } else {
166 Ok(expr.to_string())
167 }
168 }
169
170 fn convert_negated_field_ref(&self, _field: &str, expr: &str) -> Result<String> {
176 self.convert_condition_not(expr)
177 }
178
179 fn supports_field_index(&self) -> bool {
185 false
186 }
187
188 fn convert_ir_detection(
192 &self,
193 det: &IrDetection,
194 state: &mut ConversionState,
195 ) -> Result<String> {
196 crate::ir_convert::default_convert_ir_detection(self, det, state)
197 }
198
199 fn convert_ir_detection_item(
201 &self,
202 item: &IrDetectionItem,
203 state: &mut ConversionState,
204 ) -> Result<String> {
205 crate::ir_convert::default_convert_ir_detection_item(self, item, state)
206 }
207
208 fn convert_ir_array_match(
212 &self,
213 field: &str,
214 quantifier: ArrayQuantifier,
215 body: &IrDetection,
216 state: &mut ConversionState,
217 ) -> Result<String> {
218 let _ = (field, quantifier, body, state);
219 Err(ConvertError::UnsupportedArrayMatching)
220 }
221
222 fn escape_and_quote_field(&self, field: &str) -> String;
225
226 fn convert_field_str(
234 &self,
235 field: &str,
236 op: IrStrOp,
237 pattern: &IrPattern,
238 case_insensitive: bool,
239 state: &mut ConversionState,
240 ) -> Result<ConvertResult>;
241
242 fn convert_field_eq_num(
244 &self,
245 field: &str,
246 value: f64,
247 state: &mut ConversionState,
248 ) -> Result<String>;
249
250 fn convert_field_eq_bool(
252 &self,
253 field: &str,
254 value: bool,
255 state: &mut ConversionState,
256 ) -> Result<String>;
257
258 fn convert_field_eq_null(&self, field: &str, state: &mut ConversionState) -> Result<String>;
260
261 fn convert_field_regex(
263 &self,
264 field: &str,
265 pattern: &str,
266 flags: RegexFlags,
267 state: &mut ConversionState,
268 ) -> Result<ConvertResult>;
269
270 fn convert_field_eq_cidr(
272 &self,
273 field: &str,
274 cidr: &str,
275 state: &mut ConversionState,
276 ) -> Result<ConvertResult>;
277
278 fn convert_field_exists(
280 &self,
281 field: &str,
282 exists: bool,
283 state: &mut ConversionState,
284 ) -> Result<String>;
285
286 fn convert_field_eq_query_expr(
288 &self,
289 field: &str,
290 expr: &str,
291 id: &str,
292 state: &mut ConversionState,
293 ) -> Result<String>;
294
295 fn convert_field_ref(
298 &self,
299 field1: &str,
300 field2: &str,
301 op: IrStrOp,
302 case_insensitive: bool,
303 state: &mut ConversionState,
304 ) -> Result<ConvertResult>;
305
306 fn convert_field_compare_op(
308 &self,
309 field: &str,
310 op: CompareOp,
311 value: f64,
312 state: &mut ConversionState,
313 ) -> Result<String>;
314
315 fn convert_keyword_str(
317 &self,
318 pattern: &IrPattern,
319 state: &mut ConversionState,
320 ) -> Result<String>;
321
322 fn convert_keyword_num(&self, value: f64, state: &mut ConversionState) -> Result<String>;
324
325 fn finish_query(
328 &self,
329 rule: &SigmaRule,
330 query: String,
331 state: &ConversionState,
332 ) -> Result<String>;
333
334 fn finalize_query(
335 &self,
336 rule: &SigmaRule,
337 query: String,
338 index: usize,
339 state: &ConversionState,
340 output_format: &str,
341 ) -> Result<String>;
342
343 fn finalize_output(&self, queries: Vec<String>, output_format: &str) -> Result<String>;
344
345 fn output_file_extension(&self, _output_format: &str) -> &str {
355 "txt"
356 }
357
358 fn supports_correlation(&self) -> bool {
361 false
362 }
363
364 fn correlation_methods(&self) -> &[(&str, &str)] {
372 &[]
373 }
374
375 fn default_correlation_method(&self) -> &str {
377 "default"
378 }
379
380 fn convert_correlation_rule(
389 &self,
390 rule: &CorrelationRule,
391 output_format: &str,
392 pipeline_state: &PipelineState,
393 ) -> Result<Vec<String>> {
394 let mut warnings = Vec::new();
395 self.convert_correlation_rule_with_warnings(
396 rule,
397 output_format,
398 pipeline_state,
399 &mut warnings,
400 )
401 }
402
403 fn convert_correlation_rule_with_warnings(
411 &self,
412 _rule: &CorrelationRule,
413 _output_format: &str,
414 _pipeline_state: &PipelineState,
415 _warnings: &mut Vec<String>,
416 ) -> Result<Vec<String>> {
417 Err(ConvertError::UnsupportedCorrelation(
418 "correlation rules not supported by this backend".into(),
419 ))
420 }
421}
422
423pub struct TextQueryConfig {
433 pub precedence: (TokenType, TokenType, TokenType),
435 pub group_expression: &'static str,
436 pub token_separator: &'static str,
437
438 pub and_token: &'static str,
440 pub or_token: &'static str,
441 pub not_token: &'static str,
442 pub eq_token: &'static str,
443
444 pub not_eq_token: Option<&'static str>,
446 pub eq_expression: Option<&'static str>,
447 pub not_eq_expression: Option<&'static str>,
448 pub convert_not_as_not_eq: bool,
449
450 pub wildcard_multi: &'static str,
452 pub wildcard_single: &'static str,
453
454 pub str_quote: &'static str,
456 pub str_quote_pattern: Option<&'static str>,
457 pub str_quote_pattern_negation: bool,
458 pub escape_char: &'static str,
459 pub add_escaped: &'static [&'static str],
460 pub filter_chars: &'static [&'static str],
461
462 pub field_quote: Option<&'static str>,
464 pub field_quote_pattern: Option<&'static str>,
465 pub field_quote_pattern_negation: bool,
466 pub field_escape: Option<&'static str>,
467 pub field_escape_pattern: Option<&'static str>,
468
469 pub startswith_expression: Option<&'static str>,
471 pub not_startswith_expression: Option<&'static str>,
472 pub startswith_expression_allow_special: bool,
473 pub endswith_expression: Option<&'static str>,
474 pub not_endswith_expression: Option<&'static str>,
475 pub endswith_expression_allow_special: bool,
476 pub contains_expression: Option<&'static str>,
477 pub not_contains_expression: Option<&'static str>,
478 pub contains_expression_allow_special: bool,
479 pub wildcard_match_expression: Option<&'static str>,
480
481 pub case_sensitive_match_expression: Option<&'static str>,
483 pub case_sensitive_startswith_expression: Option<&'static str>,
484 pub case_sensitive_endswith_expression: Option<&'static str>,
485 pub case_sensitive_contains_expression: Option<&'static str>,
486
487 pub re_expression: Option<&'static str>,
489 pub not_re_expression: Option<&'static str>,
490 pub re_escape_char: Option<&'static str>,
491 pub re_escape: &'static [&'static str],
492 pub re_escape_escape_char: Option<&'static str>,
493
494 pub cidr_expression: Option<&'static str>,
496 pub not_cidr_expression: Option<&'static str>,
497
498 pub field_null_expression: &'static str,
500 pub field_exists_expression: Option<&'static str>,
501 pub field_not_exists_expression: Option<&'static str>,
502
503 pub compare_op_expression: Option<&'static str>,
505 pub compare_ops: &'static [(&'static str, &'static str)],
506
507 pub convert_or_as_in: bool,
509 pub convert_and_as_in: bool,
510 pub in_expressions_allow_wildcards: bool,
511 pub field_in_list_expression: Option<&'static str>,
512 pub or_in_operator: Option<&'static str>,
513 pub and_in_operator: Option<&'static str>,
514 pub list_separator: &'static str,
515
516 pub unbound_value_str_expression: Option<&'static str>,
518 pub unbound_value_num_expression: Option<&'static str>,
519 pub unbound_value_re_expression: Option<&'static str>,
520
521 pub field_eq_field_expression: Option<&'static str>,
523 pub field_eq_field_escaping_quoting: bool,
524
525 pub deferred_start: Option<&'static str>,
527 pub deferred_separator: Option<&'static str>,
528 pub deferred_only_query: &'static str,
529
530 pub bool_true: &'static str,
532 pub bool_false: &'static str,
533
534 pub query_expression: &'static str,
536 pub state_defaults: &'static [(&'static str, &'static str)],
537}
538
539impl TextQueryConfig {
540 pub fn needs_grouping(&self, outer: TokenType, inner: TokenType) -> bool {
544 if outer == TokenType::NOT {
545 return true;
546 }
547 let rank = |t: TokenType| -> u8 {
548 if t == self.precedence.0 {
549 0
550 } else if t == self.precedence.1 {
551 1
552 } else {
553 2
554 }
555 };
556 rank(inner) > rank(outer)
557 }
558}
559
560pub fn text_escape_and_quote_field(cfg: &TextQueryConfig, field: &str) -> String {
566 let mut escaped = field.to_string();
567
568 if let Some(esc) = cfg.field_escape
569 && let Some(pat) = cfg.field_escape_pattern
570 && let Some(re) = get_cached_regex(pat)
571 {
572 escaped = re
573 .replace_all(&escaped, |_: ®ex::Captures| esc)
574 .to_string();
575 }
576
577 if let Some(quote) = cfg.field_quote {
578 let should_quote = match cfg.field_quote_pattern {
579 Some(pat) => {
580 let matches = get_cached_regex(pat)
581 .map(|re| re.is_match(&escaped))
582 .unwrap_or(false);
583 if cfg.field_quote_pattern_negation {
584 !matches
585 } else {
586 matches
587 }
588 }
589 None => true,
590 };
591 if should_quote {
592 return format!("{quote}{escaped}{quote}");
593 }
594 }
595
596 escaped
597}
598
599pub fn text_convert_value_re(cfg: &TextQueryConfig, regex_str: &str) -> String {
601 let mut result = regex_str.to_string();
602
603 if let Some(esc_esc) = cfg.re_escape_escape_char
604 && let Some(esc) = cfg.re_escape_char
605 {
606 result = result.replace(esc, &format!("{esc_esc}{esc}"));
607 }
608
609 if let Some(esc) = cfg.re_escape_char {
610 for pattern in cfg.re_escape {
611 result = result.replace(pattern, &format!("{esc}{pattern}"));
612 }
613 }
614
615 result
616}
617
618pub fn text_convert_condition_group(
620 cfg: &TextQueryConfig,
621 expr: &str,
622 outer: TokenType,
623 inner: TokenType,
624) -> String {
625 if cfg.needs_grouping(outer, inner) {
626 cfg.group_expression.replace("{expr}", expr)
627 } else {
628 expr.to_string()
629 }
630}
631
632pub fn text_convert_condition_and(cfg: &TextQueryConfig, exprs: &[String]) -> String {
634 let sep = if cfg.and_token.is_empty() {
635 cfg.token_separator.to_string()
636 } else {
637 format!(
638 "{}{}{}",
639 cfg.token_separator, cfg.and_token, cfg.token_separator
640 )
641 };
642 exprs.join(&sep)
643}
644
645pub fn text_convert_condition_or(cfg: &TextQueryConfig, exprs: &[String]) -> String {
647 let sep = format!(
648 "{}{}{}",
649 cfg.token_separator, cfg.or_token, cfg.token_separator
650 );
651 exprs.join(&sep)
652}
653
654pub fn text_convert_condition_not(cfg: &TextQueryConfig, expr: &str) -> String {
656 format!("{}{}{expr}", cfg.not_token, cfg.token_separator)
657}
658
659pub fn text_finish_query(
661 cfg: &TextQueryConfig,
662 query: &str,
663 state: &ConversionState,
664 rule: &SigmaRule,
665) -> String {
666 let main_query = if state.has_deferred() && query.is_empty() {
667 cfg.deferred_only_query
668 } else {
669 query
670 };
671
672 let mut result = cfg.query_expression.replace("{query}", main_query);
673
674 for (key, default) in cfg.state_defaults {
676 let placeholder = format!("{{{key}}}");
677 result = result.replace(&placeholder, default);
678 }
679 for (key, val) in &state.processing_state {
680 if let Some(s) = val.as_str() {
681 let placeholder = format!("{{{key}}}");
682 result = result.replace(&placeholder, s);
683 }
684 }
685
686 result = result.replace("{rule.title}", &rule.title);
688 if let Some(id) = &rule.id {
689 result = result.replace("{rule.id}", id);
690 }
691
692 if state.has_deferred() {
694 let deferred_start = cfg.deferred_start.unwrap_or("");
695 let deferred_sep = cfg.deferred_separator.unwrap_or("");
696 let parts: Vec<String> = state.deferred.iter().map(|d| d.finalize()).collect();
697 result = format!("{result}{deferred_start}{}", parts.join(deferred_sep));
698 }
699
700 result
701}
702
703pub fn text_convert_ir_pattern(cfg: &TextQueryConfig, pattern: &IrPattern) -> String {
715 let mut result = String::new();
716 let mut has_wildcards = false;
717
718 for part in &pattern.parts {
719 match part {
720 IrPatternPart::Literal(s) => {
721 let mut escaped = String::with_capacity(s.len());
722 for ch in s.chars() {
723 let ch_str = ch.to_string();
724 if cfg.filter_chars.contains(&ch_str.as_str()) {
725 continue;
726 }
727 if ch_str == cfg.escape_char
728 || ch_str == cfg.str_quote
729 || cfg.add_escaped.contains(&ch_str.as_str())
730 {
731 escaped.push_str(cfg.escape_char);
732 }
733 escaped.push(ch);
734 }
735 result.push_str(&escaped);
736 }
737 IrPatternPart::WildcardMulti => {
738 result.push_str(cfg.wildcard_multi);
739 has_wildcards = true;
740 }
741 IrPatternPart::WildcardSingle => {
742 result.push_str(cfg.wildcard_single);
743 has_wildcards = true;
744 }
745 }
746 }
747
748 if !has_wildcards {
749 let should_quote = match cfg.str_quote_pattern {
750 Some(pat) => {
751 let matches = get_cached_regex(pat)
752 .map(|re| re.is_match(&result))
753 .unwrap_or(false);
754 if cfg.str_quote_pattern_negation {
755 !matches
756 } else {
757 matches
758 }
759 }
760 None => true,
761 };
762 if should_quote {
763 return format!("{}{result}{}", cfg.str_quote, cfg.str_quote);
764 }
765 }
766
767 result
768}
769
770pub fn text_convert_field_str_ir(
773 cfg: &TextQueryConfig,
774 field: &str,
775 op: IrStrOp,
776 pattern: &IrPattern,
777 case_insensitive: bool,
778) -> Result<ConvertResult> {
779 let escaped_field = text_escape_and_quote_field(cfg, field);
780 let is_cased = !case_insensitive;
781 let is_contains = op == IrStrOp::Contains;
782 let is_startswith = op == IrStrOp::StartsWith;
783 let is_endswith = op == IrStrOp::EndsWith;
784
785 let value_str = text_convert_ir_pattern(cfg, pattern);
786
787 if is_cased {
788 if is_contains && let Some(expr) = cfg.case_sensitive_contains_expression {
789 return Ok(ConvertResult::Query(
790 expr.replace("{field}", &escaped_field)
791 .replace("{value}", &value_str),
792 ));
793 }
794 if is_startswith && let Some(expr) = cfg.case_sensitive_startswith_expression {
795 return Ok(ConvertResult::Query(
796 expr.replace("{field}", &escaped_field)
797 .replace("{value}", &value_str),
798 ));
799 }
800 if is_endswith && let Some(expr) = cfg.case_sensitive_endswith_expression {
801 return Ok(ConvertResult::Query(
802 expr.replace("{field}", &escaped_field)
803 .replace("{value}", &value_str),
804 ));
805 }
806 if let Some(expr) = cfg.case_sensitive_match_expression {
807 return Ok(ConvertResult::Query(
808 expr.replace("{field}", &escaped_field)
809 .replace("{value}", &value_str),
810 ));
811 }
812 }
813
814 if is_contains && let Some(expr) = cfg.contains_expression {
815 return Ok(ConvertResult::Query(
816 expr.replace("{field}", &escaped_field)
817 .replace("{value}", &value_str),
818 ));
819 }
820 if is_startswith && let Some(expr) = cfg.startswith_expression {
821 return Ok(ConvertResult::Query(
822 expr.replace("{field}", &escaped_field)
823 .replace("{value}", &value_str),
824 ));
825 }
826 if is_endswith && let Some(expr) = cfg.endswith_expression {
827 return Ok(ConvertResult::Query(
828 expr.replace("{field}", &escaped_field)
829 .replace("{value}", &value_str),
830 ));
831 }
832
833 if pattern.has_wildcards()
834 && let Some(expr) = cfg.wildcard_match_expression
835 {
836 return Ok(ConvertResult::Query(
837 expr.replace("{field}", &escaped_field)
838 .replace("{value}", &value_str),
839 ));
840 }
841
842 let result = if let Some(expr) = cfg.eq_expression {
843 expr.replace("{field}", &escaped_field)
844 .replace("{value}", &value_str)
845 } else {
846 format!("{escaped_field}{}{value_str}", cfg.eq_token)
847 };
848 Ok(ConvertResult::Query(result))
849}