1"""The `suricata_check.utils.checker` module contains several utilities for developing rule checkers."""
2
3import logging
4from collections.abc import Iterable, Sequence
5from functools import lru_cache
6from typing import Optional, Union
7
8from suricata_check.utils.regex import (
9 ALL_DETECTION_KEYWORDS,
10 ALL_KEYWORDS,
11 ALL_METADATA_KEYWORDS,
12 BUFFER_KEYWORDS,
13 STICKY_BUFFER_NAMING,
14 get_variable_groups,
15)
16from suricata_check.utils.regex_provider import Pattern, get_regex_provider
17from suricata_check.utils.rule import Rule
18
19_LRU_CACHE_SIZE = 10
20
21
22_logger = logging.getLogger(__name__)
23
24_regex_provider = get_regex_provider()
25
26
[docs]
27def check_rule_option_recognition(rule: Rule) -> None:
28 """Checks whether all rule options and metadata options are recognized.
29
30 Unrecognized options will be logged as a warning in `suricata-check.log`
31 """
32 for option in rule.options:
33 name = option.name
34 if name not in ALL_KEYWORDS:
35 _logger.warning(
36 "Option %s from rule %s is not recognized.",
37 name,
38 get_rule_option(rule, "sid"),
39 )
40
41 for option in rule.metadata:
42 name = _regex_provider.split(r"\s+", option)[0]
43 if name not in ALL_METADATA_KEYWORDS:
44 _logger.warning(
45 "Metadata option %s from rule %s is not recognized.",
46 name,
47 get_rule_option(rule, "sid"),
48 )
49
50
[docs]
51def is_rule_option_set(rule: Rule, name: str) -> bool:
52 """Checks whether a rule has a certain option set.
53
54 Args:
55 rule (suricata_check.utils.rule.Rule): rule to be inspected
56 name (str): name of the option
57
58 Returns:
59 bool: True iff the option is set atleast once
60
61 """
62 return __is_rule_option_set(rule, name)
63
64
65@lru_cache(maxsize=_LRU_CACHE_SIZE)
66def __is_rule_option_set(rule: Rule, name: str) -> bool:
67 if name not in (
68 "action",
69 "proto",
70 "source_addr",
71 "source_port",
72 "direction",
73 "dest_addr",
74 "dest_port",
75 ):
76 if name not in ALL_KEYWORDS:
77 _logger.warning("Requested a non-recognized keyword: %s", name)
78
79 for option in rule.options:
80 if option.name == name:
81 return True
82
83 return False
84
85 if not hasattr(rule, name):
86 return False
87
88 if getattr(rule, name) is None:
89 return False
90
91 if getattr(rule, name) == "":
92 return False
93
94 return True
95
96
[docs]
97def get_rule_suboptions(
98 rule: Rule,
99 name: str,
100 warn: bool = True,
101) -> Sequence[tuple[str, Optional[str]]]:
102 """Returns a list of suboptions set in a rule."""
103 values = get_rule_options(rule, name)
104 valid_suboptions: list[tuple[str, Optional[str]]] = []
105 for value in values:
106 if value is None:
107 continue
108 values = value.split(",")
109 suboptions: list[Optional[tuple[str, Optional[str]]]] = [
110 __split_suboption(suboption, warn=warn) for suboption in values
111 ]
112 # Filter out suboptions that could not be parsed
113 valid_suboptions += [
114 suboption for suboption in suboptions if suboption is not None
115 ]
116
117 return valid_suboptions
118
119
120def __split_suboption(
121 suboption: str,
122 warn: bool,
123) -> Optional[tuple[str, Optional[str]]]:
124 suboption = suboption.strip()
125
126 splitted = suboption.split(" ")
127 splitted = [s.strip() for s in splitted]
128
129 if len(splitted) == 1:
130 return (splitted[0], None)
131 if len(splitted) == 2: # noqa: PLR2004
132 return tuple(splitted) # type: ignore reportReturnType
133
134 if warn:
135 _logger.warning("Failed to split suboption: %s", suboption)
136
137 return None
138
139
[docs]
140def is_rule_suboption_set(rule: Rule, name: str, sub_name: str) -> bool:
141 """Checks if a suboption within an option is set."""
142 suboptions = get_rule_suboptions(rule, name)
143 _logger.debug(suboptions)
144
145 if sub_name in [suboption[0] for suboption in suboptions]:
146 return True
147
148 return False
149
150
[docs]
151def get_rule_suboption(rule: Rule, name: str, sub_name: str) -> Optional[str]:
152 """Returns a suboption within an option is set."""
153 suboptions = get_rule_suboptions(rule, name)
154
155 for suboption in suboptions:
156 if sub_name == suboption[0]:
157 return suboption[1]
158
159 msg = f"Option {name} not found in rule."
160 _logger.debug(msg)
161 return None
162
163
[docs]
164def count_rule_options(
165 rule: Rule,
166 name: Union[str, Iterable[str]],
167) -> int:
168 """Counts how often an option is set in a rule.
169
170 Args:
171 rule (suricata_check.utils.rule.Rule): rule to be inspected
172 name (Union[str, Iterable[str]]): name or names of the option
173
174 Returns:
175 int: The number of times an option is set
176
177 """
178 if not isinstance(name, str):
179 name = tuple(sorted(name))
180 return __count_rule_options(rule, name)
181
182
183@lru_cache(maxsize=_LRU_CACHE_SIZE)
184def __count_rule_options(
185 rule: Rule,
186 name: Union[str, Iterable[str]],
187) -> int:
188 count = 0
189
190 if not isinstance(name, str):
191 for single_name in name:
192 count += count_rule_options(rule, single_name)
193 return count
194
195 if name not in (
196 "action",
197 "proto",
198 "source_addr",
199 "source_port",
200 "direction",
201 "dest_addr",
202 "dest_port",
203 ):
204 if name not in ALL_KEYWORDS:
205 _logger.warning("Requested a non-recognized keyword: %s", name)
206
207 for option in rule.options:
208 if option.name == name:
209 count += 1
210
211 if count == 0 and is_rule_option_set(rule, name):
212 count = 1
213
214 return count
215
216
[docs]
217def get_rule_option(rule: Rule, name: str) -> Optional[str]:
218 """Retrieves one option of a rule with a certain name.
219
220 If an option is set multiple times, it returns only one indeterminately.
221
222 Args:
223 rule (suricata_check.utils.rule.Rule): rule to be inspected
224 name (str): name of the option
225
226 Returns:
227 Optional[str]: The value of the option or None if it was not set.
228
229 """
230 return __get_rule_option(rule, name)
231
232
233@lru_cache(maxsize=_LRU_CACHE_SIZE)
234def __get_rule_option(rule: Rule, name: str) -> Optional[str]:
235 options = get_rule_options(rule, name)
236
237 if len(options) == 0:
238 msg = f"Option {name} not found in rule."
239 _logger.debug(msg)
240 return None
241
242 if len(options) == 0:
243 msg = f"Cannot unambiguously determine the value of {name} because it is set multiple times."
244 _logger.warning(msg)
245 return None
246
247 return options[0]
248
249
[docs]
250def get_rule_options(
251 rule: Rule,
252 name: Union[str, Iterable[str]],
253) -> Sequence[Optional[str]]:
254 """Retrieves all options of a rule with a certain name.
255
256 Args:
257 rule (suricata_check.utils.rule.Rule): rule to be inspected
258 name (Union[str, Iterable[str]]): name or names of the option
259
260 Returns:
261 Sequence[str]: The values of the option.
262
263 """
264 if not isinstance(name, str):
265 name = tuple(sorted(name))
266 return __get_rule_options(rule, name)
267
268
269@lru_cache(maxsize=_LRU_CACHE_SIZE)
270def __get_rule_options(
271 rule: Rule,
272 name: Union[str, Iterable[str]],
273 warn_not_found: bool = True,
274) -> Sequence[str]:
275 values = []
276
277 if not isinstance(name, str):
278 for single_name in name:
279 values.extend(__get_rule_options(rule, single_name, warn_not_found=False))
280 return values
281
282 if name not in (
283 "action",
284 "proto",
285 "source_addr",
286 "source_port",
287 "direction",
288 "dest_addr",
289 "dest_port",
290 ):
291 if name not in ALL_KEYWORDS:
292 _logger.warning("Requested a non-recognized keyword: %s", name)
293
294 for option in rule.options:
295 if option.name == name:
296 values.append(option.value)
297 elif hasattr(rule, name):
298 values.append(getattr(rule, name))
299
300 if warn_not_found and len(values) == 0:
301 msg = f"Option {name} not found in rule {rule}."
302 _logger.debug(msg)
303
304 return values
305
306
[docs]
307def is_rule_option_equal_to(rule: Rule, name: str, value: str) -> bool:
308 """Checks whether a rule has a certain option set to a certain value.
309
310 If the option is set multiple times, it will return True if atleast one option matches the value.
311
312 Args:
313 rule (suricata_check.utils.rule.Rule): rule to be inspected
314 name (str): name of the option
315 value (str): value to check for
316
317 Returns:
318 bool: True iff the rule has the option set to the value atleast once
319
320 """
321 if not is_rule_option_set(rule, name):
322 return False
323
324 values = get_rule_options(rule, name)
325
326 for val in values:
327 if val == value:
328 return True
329
330 return False
331
332
[docs]
333def is_rule_suboption_equal_to(
334 rule: Rule,
335 name: str,
336 sub_name: str,
337 value: str,
338) -> bool:
339 """Checks whether a rule has a certain suboption set to a certain value.
340
341 If the suboption is set multiple times, it will return True if atleast one option matches the value.
342
343 Args:
344 rule (suricata_check.utils.rule.Rule): rule to be inspected
345 name (str): name of the option
346 sub_name (str): name of the suboption
347 value (str): value to check for
348
349 Returns:
350 bool: True iff the rule has the option set to the value atleast once
351
352 """
353 if not is_rule_suboption_set(rule, name, sub_name):
354 return False
355
356 values = get_rule_suboptions(rule, name)
357
358 for key, val in values:
359 if key == sub_name and val == value:
360 return True
361
362 return False
363
364
[docs]
365def is_rule_option_equal_to_regex(
366 rule: Rule,
367 name: str,
368 regex, # re.Pattern or regex.Pattern # noqa: ANN001
369) -> bool:
370 """Checks whether a rule has a certain option set to match a certain regex.
371
372 If the option is set multiple times, it will return True if atleast one option matches the regex.
373
374 Args:
375 rule (suricata_check.utils.rule.Rule): rule to be inspected
376 name (str): name of the option
377 regex (Union[re.Pattern, regex.Pattern]): regex to check for
378
379 Returns:
380 bool: True iff the rule has atleast one option matching the regex
381
382 """
383 if not is_rule_option_set(rule, name):
384 return False
385
386 values = get_rule_options(rule, name)
387
388 for value in values:
389 if value is None:
390 continue
391 if regex.match(value) is not None:
392 return True
393
394 return False
395
396
[docs]
397def is_rule_suboption_equal_to_regex(
398 rule: Rule,
399 name: str,
400 sub_name: str,
401 regex, # re.Pattern or regex.Pattern # noqa: ANN001
402) -> bool:
403 """Checks whether a rule has a certain option set to match a certain regex.
404
405 If the option is set multiple times, it will return True if atleast one option matches the regex.
406
407 Args:
408 rule (suricata_check.utils.rule.Rule): rule to be inspected
409 name (str): name of the option
410 sub_name (str): name of the suboption
411 regex (Union[re.Pattern, regex.Pattern]): regex to check for
412
413 Returns:
414 bool: True iff the rule has atleast one option matching the regex
415
416 """
417 if not is_rule_suboption_set(rule, name, sub_name):
418 return False
419
420 values = get_rule_suboptions(rule, name)
421
422 for key, value in values:
423 if key == sub_name and regex.match(value) is not None:
424 return True
425
426 return False
427
428
[docs]
429def is_rule_option_always_equal_to_regex(
430 rule: Rule,
431 name: str,
432 regex, # re.Pattern or regex.Pattern # noqa: ANN001
433) -> Optional[bool]:
434 """Checks whether a rule has a certain option set to match a certain regex.
435
436 If the option is set multiple times, it will return True if all options match the regex.
437 Returns none if the rule option is not set.
438
439 Args:
440 rule (suricata_check.utils.rule.Rule): rule to be inspected
441 name (str): name of the option
442 regex (Union[re.Pattern, regex.Pattern]): regex to check for
443
444 Returns:
445 bool: True iff the rule has all options matching the regex
446
447 """
448 if not is_rule_option_set(rule, name):
449 return None
450
451 values = get_rule_options(rule, name)
452
453 for value in values:
454 if value is None:
455 return False
456 if regex.match(value) is None:
457 return False
458
459 return True
460
461
[docs]
462def is_rule_suboption_always_equal_to_regex(
463 rule: Rule,
464 name: str,
465 sub_name: str,
466 regex, # re.Pattern or regex.Pattern # noqa: ANN001
467) -> Optional[bool]:
468 """Checks whether a rule has a certain option set to match a certain regex.
469
470 If the option is set multiple times, it will return True if all options match the regex.
471 Returns none if the rule option is not set.
472
473 Args:
474 rule (suricata_check.utils.rule.Rule): rule to be inspected
475 name (str): name of the option
476 sub_name (str): name of the suboption
477 regex (Union[re.Pattern, regex.Pattern]): regex to check for
478
479 Returns:
480 bool: True iff the rule has all options matching the regex
481
482 """
483 if not is_rule_suboption_set(rule, name, sub_name):
484 return None
485
486 values = get_rule_suboptions(rule, name)
487
488 for key, value in values:
489 if key == sub_name and regex.match(value) is None:
490 return False
491
492 return True
493
494
[docs]
495def are_rule_options_equal_to_regex(
496 rule: Rule,
497 names: Iterable[str],
498 regex, # re.Pattern or regex.Pattern # noqa: ANN001
499) -> bool:
500 """Checks whether a rule has certain options set to match a certain regex.
501
502 If multiple options are set, it will return True if atleast one option matches the regex.
503
504 Args:
505 rule (suricata_check.utils.rule.Rule): rule to be inspected
506 names (Iterable[str]): names of the options
507 regex (Union[re.Pattern, regex.Pattern]): regex to check for
508
509 Returns:
510 bool: True iff the rule has atleast one option matching the regex
511
512 """
513 for name in names:
514 if is_rule_option_equal_to_regex(rule, name, regex):
515 return True
516
517 return False
518
519
[docs]
520def is_rule_option_one_of(
521 rule: Rule,
522 name: str,
523 possible_values: Union[Sequence[str], set[str]],
524) -> bool:
525 """Checks whether a rule has a certain option set to a one of certain values.
526
527 If the option is set multiple times, it will return True if atleast one option matches a value.
528
529 Args:
530 rule (suricata_check.utils.rule.Rule): rule to be inspected
531 name (str): name of the option
532 possible_values (Iterable[str]): values to check for
533
534 Returns:
535 bool: True iff the rule has the option set to one of the values atleast once
536
537 """
538 if not is_rule_option_set(rule, name):
539 return False
540
541 values = get_rule_options(rule, name)
542
543 for value in values:
544 if value is None:
545 continue
546 if value in possible_values:
547 return True
548
549 return False
550
551
[docs]
552def get_rule_sticky_buffer_naming(
553 rule: Rule,
554) -> list[tuple[str, str]]:
555 """Returns a list of tuples containing the name of a sticky buffer, and the modifier alternative."""
556 sticky_buffer_naming = []
557 for option in rule.options:
558 if option.name in STICKY_BUFFER_NAMING:
559 sticky_buffer_naming.append(
560 (option.name, STICKY_BUFFER_NAMING[option.name]),
561 )
562
563 return sticky_buffer_naming
564
565
[docs]
566def get_all_variable_groups(
567 rule: Rule,
568) -> list[str]:
569 """Returns a list of variable groups such as $HTTP_SERVERS in a rule."""
570 return __get_all_variable_groups(rule)
571
572
573@lru_cache(maxsize=_LRU_CACHE_SIZE)
574def __get_all_variable_groups(
575 rule: Rule,
576) -> list[str]:
577 variable_groups = []
578 for name in (
579 "source_addr",
580 "source_port",
581 "direction",
582 "dest_addr",
583 "dest_port",
584 ):
585 if is_rule_option_set(rule, name):
586 value = get_rule_option(rule, name)
587 assert value is not None
588 variable_groups += get_variable_groups(value)
589
590 return variable_groups
591
592
[docs]
593def get_rule_option_positions(
594 rule: Rule,
595 name: str,
596 sequence: Optional[tuple[str, ...]] = None,
597) -> Sequence[int]:
598 """Finds the positions of an option in the rule body.
599
600 Optionally takes a sequence of options to use instead of `rule['options']`.
601 """
602 return __get_rule_option_positions(rule, name, sequence=sequence)
603
604
605@lru_cache(maxsize=_LRU_CACHE_SIZE)
606def __get_rule_option_positions(
607 rule: Rule,
608 name: str,
609 sequence: Optional[tuple[str, ...]] = None,
610) -> Sequence[int]:
611 provided_sequence = True
612 if sequence is None:
613 sequence = tuple(option.name for option in rule.options)
614 provided_sequence = False
615
616 positions = []
617 for i, option in enumerate(sequence):
618 if option == name:
619 positions.append(i)
620
621 if not provided_sequence and len(positions) == 0 and is_rule_option_set(rule, name):
622 msg = f"Cannot determine position of {name} option since it is not part of the sequence of detection keywords."
623 _logger.critical(msg)
624 raise ValueError(msg)
625
626 return tuple(sorted(positions))
627
628
[docs]
629def get_rule_option_position(rule: Rule, name: str) -> Optional[int]:
630 """Finds the position of an option in the rule body.
631
632 Return None if the option is not set or set multiple times.
633 """
634 return __get_rule_option_position(rule, name)
635
636
637@lru_cache(maxsize=_LRU_CACHE_SIZE)
638def __get_rule_option_position(rule: Rule, name: str) -> Optional[int]:
639 positions = get_rule_option_positions(rule, name)
640
641 if len(positions) == 0:
642 _logger.debug(
643 "Cannot unambigously determine the position of the %s option since it it not set.",
644 name,
645 )
646 return None
647
648 if len(positions) == 1:
649 return positions[0]
650
651 _logger.debug(
652 "Cannot unambigously determine the position of the %s option since it is set multiple times.",
653 name,
654 )
655 return None
656
657
[docs]
658def is_rule_option_first(rule: Rule, name: str) -> Optional[int]:
659 """Checks if a rule option is positioned at the beginning of the body."""
660 position = get_rule_option_position(rule, name)
661
662 if position is None:
663 _logger.debug("Cannot unambiguously determine if option %s first.", name)
664 return None
665
666 if position == 0:
667 return True
668
669 return False
670
671
[docs]
672def is_rule_option_last(rule: Rule, name: str) -> Optional[bool]:
673 """Checks if a rule option is positioned at the end of the body."""
674 position = get_rule_option_position(rule, name)
675
676 if position is None:
677 _logger.debug("Cannot unambiguously determine if option %s last.", name)
678 return None
679
680 if position == len(rule.options) - 1:
681 return True
682
683 return False
684
685
[docs]
686def get_rule_options_positions(
687 rule: Rule,
688 names: Iterable[str],
689 sequence: Optional[Iterable[str]] = None,
690) -> Iterable[int]:
691 """Finds the positions of several options in the rule body."""
692 return __get_rule_options_positions(
693 rule,
694 tuple(sorted(names)),
695 sequence=tuple(sequence) if sequence else None,
696 )
697
698
699@lru_cache(maxsize=_LRU_CACHE_SIZE)
700def __get_rule_options_positions(
701 rule: Rule,
702 names: Iterable[str],
703 sequence: Optional[tuple[str, ...]] = None,
704) -> Iterable[int]:
705 positions = []
706
707 for name in names:
708 positions.extend(get_rule_option_positions(rule, name, sequence=sequence))
709
710 return tuple(sorted(positions))
711
712
[docs]
713def is_rule_option_put_before(
714 rule: Rule,
715 name: str,
716 other_names: Union[Sequence[str], set[str]],
717 sequence: Optional[Iterable[str]] = None,
718) -> Optional[bool]:
719 """Checks whether a rule option is placed before one or more other options."""
720 return __is_rule_option_put_before(
721 rule,
722 name,
723 tuple(sorted(other_names)),
724 sequence=tuple(sequence) if sequence else None,
725 )
726
727
728@lru_cache(maxsize=_LRU_CACHE_SIZE)
729def __is_rule_option_put_before(
730 rule: Rule,
731 name: str,
732 other_names: Union[Sequence[str], set[str]],
733 sequence: Optional[tuple[str, ...]] = None,
734) -> Optional[bool]:
735 if len(other_names) == 0:
736 _logger.debug(
737 "Cannot unambiguously determine if option %s is put before empty Iterable of other options.",
738 name,
739 )
740 return None
741
742 positions = get_rule_option_positions(rule, name, sequence=sequence)
743
744 if name in other_names:
745 _logger.debug("Excluding name %s from other_names because of overlap.", name)
746 other_names = set(other_names).difference({name})
747
748 other_positions = get_rule_options_positions(rule, other_names, sequence=sequence)
749
750 for other_position in other_positions:
751 for position in positions:
752 if position < other_position:
753 return True
754 return False
755
756
[docs]
757def is_rule_option_always_put_before(
758 rule: Rule,
759 name: str,
760 other_names: Union[Sequence[str], set[str]],
761 sequence: Optional[Iterable[str]] = None,
762) -> Optional[bool]:
763 """Checks whether a rule option is placed before one or more other options."""
764 return __is_rule_option_always_put_before(
765 rule,
766 name,
767 tuple(sorted(other_names)),
768 sequence=tuple(sequence) if sequence else None,
769 )
770
771
772@lru_cache(maxsize=_LRU_CACHE_SIZE)
773def __is_rule_option_always_put_before(
774 rule: Rule,
775 name: str,
776 other_names: Union[Sequence[str], set[str]],
777 sequence: Optional[tuple[str, ...]] = None,
778) -> Optional[bool]:
779 if len(other_names) == 0:
780 _logger.debug(
781 "Cannot unambiguously determine if option %s is put before empty Iterable of other options.",
782 name,
783 )
784 return None
785
786 positions = get_rule_option_positions(rule, name, sequence=sequence)
787
788 if name in other_names:
789 _logger.debug("Excluding name %s from other_names because of overlap.", name)
790 other_names = set(other_names).difference({name})
791
792 other_positions = get_rule_options_positions(rule, other_names, sequence=sequence)
793
794 for other_position in other_positions:
795 for position in positions:
796 if position >= other_position:
797 return False
798 return True
799
800
[docs]
801def are_rule_options_put_before(
802 rule: Rule,
803 names: Union[Sequence[str], set[str]],
804 other_names: Union[Sequence[str], set[str]],
805 sequence: Optional[Iterable[str]] = None,
806) -> Optional[bool]:
807 """Checks whether rule options are placed before one or more other options."""
808 if len(other_names) == 0:
809 _logger.debug(
810 "Cannot unambiguously determine if an empty Iterable of options are put before other options %s.",
811 other_names,
812 )
813 return None
814 if len(other_names) == 0:
815 _logger.debug(
816 "Cannot unambiguously determine if options %s are put before empty Iterable of other options.",
817 names,
818 )
819 return None
820
821 for name in names:
822 if is_rule_option_put_before(rule, name, other_names, sequence=sequence):
823 return True
824 return False
825
826
[docs]
827def are_rule_options_always_put_before(
828 rule: Rule,
829 names: Iterable[str],
830 other_names: Sequence[str],
831 sequence: Optional[Iterable[str]] = None,
832) -> Optional[bool]:
833 """Checks whether rule options are placed before one or more other options."""
834 if len(other_names) == 0:
835 _logger.debug(
836 "Cannot unambiguously determine if an empty Iterable of options are put before other options %s.",
837 other_names,
838 )
839 return None
840 if len(other_names) == 0:
841 _logger.debug(
842 "Cannot unambiguously determine if options %s are put before empty Iterable of other options.",
843 names,
844 )
845 return None
846
847 for name in names:
848 if not is_rule_option_put_before(rule, name, other_names, sequence=sequence):
849 return False
850 return True
851
852
[docs]
853def select_rule_options_by_regex(rule: Rule, regex: Pattern) -> Iterable[str]:
854 """Selects rule options present in rule matching a regular expression."""
855 return __select_rule_options_by_regex(rule, regex)
856
857
858@lru_cache(maxsize=_LRU_CACHE_SIZE)
859def __select_rule_options_by_regex(
860 rule: Rule,
861 regex: Pattern,
862) -> Iterable[str]:
863 options = []
864
865 for option in rule.options:
866 name = option.name
867 if regex.match(name):
868 options.append(name)
869
870 return tuple(sorted(options))
871
872
[docs]
873def get_rule_keyword_sequences(
874 rule: Rule,
875 seperator_keywords: Iterable[str] = BUFFER_KEYWORDS,
876 included_keywords: Iterable[str] = ALL_DETECTION_KEYWORDS,
877) -> Sequence[tuple[str, ...]]:
878 """Returns a sequence of sequences of detection options in a rule."""
879 sequences: list[list[str]] = []
880
881 # Relies on the assumption that the order of options in the rule is preserved while parsing
882 sequence_i = -1
883 first_seperator_seen = False
884 for option in rule.options:
885 name = option.name
886 if name in seperator_keywords:
887 if not first_seperator_seen:
888 if len(sequences) > 0:
889 sequences[sequence_i].append(name)
890 else:
891 sequence_i += 1
892 sequences.append([name])
893 else:
894 sequence_i += 1
895 sequences.append([name])
896 first_seperator_seen = True
897 elif name in included_keywords and sequence_i == -1:
898 sequence_i += 1
899 sequences.append([name])
900 elif name in included_keywords:
901 sequences[sequence_i].append(name)
902
903 if len(sequences) == 0:
904 _logger.debug(
905 "No sequences found separated by %s in rule %s",
906 seperator_keywords,
907 rule.raw,
908 )
909 return ()
910
911 for sequence in sequences:
912 assert len(sequence) > 0
913
914 result = tuple(tuple(sequence) for sequence in sequences)
915
916 _logger.debug(
917 "Detected sequences %s separated by %s in rule %s",
918 result,
919 seperator_keywords,
920 rule.raw,
921 )
922
923 return result