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