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
8import idstools.rule
9
10from suricata_check.utils.regex import (
11 ALL_DETECTION_KEYWORDS,
12 ALL_KEYWORDS,
13 ALL_METADATA_KEYWORDS,
14 BUFFER_KEYWORDS,
15 STICKY_BUFFER_NAMING,
16 get_regex_provider,
17 get_variable_groups,
18)
19
20_LRU_CACHE_SIZE = 10
21
22
23_logger = logging.getLogger(__name__)
24
25_regex_provider = get_regex_provider()
26
27
[docs]
28def check_rule_option_recognition(rule: idstools.rule.Rule) -> None:
29 """Checks whether all rule options and metadata options are recognized.
30
31 Unrecognized options will be logged as a warning in `suricata-check.log`
32 """
33 for option in rule["options"]:
34 name = option["name"]
35 if name not in ALL_KEYWORDS:
36 _logger.warning(
37 "Option %s from rule %s is not recognized.",
38 name,
39 rule["sid"],
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 from rule %s is not recognized.",
47 name,
48 rule["sid"],
49 )
50
51
[docs]
52def is_rule_option_set(rule: idstools.rule.Rule, name: str) -> bool:
53 """Checks whether a rule has a certain option set.
54
55 Args:
56 rule (idstools.rule.Rule): rule to be inspected
57 name (str): name of the option
58
59 Returns:
60 bool: True iff the option is set atleast once
61
62 """
63 return __is_rule_option_set(rule, name)
64
65
66@lru_cache(maxsize=_LRU_CACHE_SIZE)
67def __is_rule_option_set(rule: idstools.rule.Rule, name: str) -> bool:
68 if name not in (
69 "action",
70 "proto",
71 "source_addr",
72 "source_port",
73 "direction",
74 "dest_addr",
75 "dest_port",
76 ):
77 if name not in ALL_KEYWORDS:
78 _logger.warning("Requested a non-recognized keyword: %s", name)
79
80 for option in rule["options"]:
81 if option["name"] == name:
82 return True
83
84 return False
85
86 if name not in rule:
87 return False
88
89 if rule[name] is None:
90 return False
91
92 if rule[name] == "":
93 return False
94
95 return True
96
97
[docs]
98def get_rule_suboptions(
99 rule: idstools.rule.Rule, name: str, warn: bool = True
100) -> Sequence[tuple[str, Optional[str]]]:
101 """Returns a list of suboptions set in a rule."""
102 values = get_rule_options(rule, name)
103 valid_suboptions: list[tuple[str, Optional[str]]] = []
104 for value in values:
105 if value is None:
106 continue
107 values = value.split(",")
108 suboptions: list[Optional[tuple[str, Optional[str]]]] = [
109 __split_suboption(suboption, warn=warn) for suboption in values
110 ]
111 # Filter out suboptions that could not be parsed
112 valid_suboptions += [
113 suboption for suboption in suboptions if suboption is not None
114 ]
115
116 return valid_suboptions
117
118
119def __split_suboption(
120 suboption: str, warn: bool
121) -> Optional[tuple[str, Optional[str]]]:
122 suboption = suboption.strip()
123
124 splitted = suboption.split(" ")
125 splitted = [s.strip() for s in splitted]
126
127 if len(splitted) == 1:
128 return (splitted[0], None)
129 if len(splitted) == 2: # noqa: PLR2004
130 return tuple(splitted) # type: ignore reportReturnType
131
132 if warn:
133 _logger.warning("Failed to split suboption: %s", suboption)
134
135 return None
136
137
[docs]
138def is_rule_suboption_set(rule: idstools.rule.Rule, name: str, sub_name: str) -> bool:
139 """Checks if a suboption within an option is set."""
140 suboptions = get_rule_suboptions(rule, name)
141 _logger.debug(suboptions)
142
143 if sub_name in [suboption[0] for suboption in suboptions]:
144 return True
145
146 return False
147
148
[docs]
149def get_rule_suboption(
150 rule: idstools.rule.Rule, name: str, sub_name: str
151) -> 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: idstools.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 (idstools.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: idstools.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: idstools.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 (idstools.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: idstools.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: idstools.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 (idstools.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: idstools.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 name in rule:
298 values.append(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: idstools.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 (idstools.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: idstools.rule.Rule, name: str, sub_name: str, value: str
335) -> bool:
336 """Checks whether a rule has a certain suboption set to a certain value.
337
338 If the suboption is set multiple times, it will return True if atleast one option matches the value.
339
340 Args:
341 rule (idstools.rule.Rule): rule to be inspected
342 name (str): name of the option
343 sub_name (str): name of the suboption
344 value (str): value to check for
345
346 Returns:
347 bool: True iff the rule has the option set to the value atleast once
348
349 """
350 if not is_rule_suboption_set(rule, name, sub_name):
351 return False
352
353 values = get_rule_suboptions(rule, name)
354
355 for key, val in values:
356 if key == sub_name and val == value:
357 return True
358
359 return False
360
361
[docs]
362def is_rule_option_equal_to_regex(
363 rule: idstools.rule.Rule,
364 name: str,
365 regex, # re.Pattern or regex.Pattern # noqa: ANN001
366) -> bool:
367 """Checks whether a rule has a certain option set to match a certain regex.
368
369 If the option is set multiple times, it will return True if atleast one option matches the regex.
370
371 Args:
372 rule (idstools.rule.Rule): rule to be inspected
373 name (str): name of the option
374 regex (Union[re.Pattern, regex.Pattern]): regex to check for
375
376 Returns:
377 bool: True iff the rule has atleast one option matching the regex
378
379 """
380 if not is_rule_option_set(rule, name):
381 return False
382
383 values = get_rule_options(rule, name)
384
385 for value in values:
386 if value is None:
387 continue
388 if regex.match(value) is not None:
389 return True
390
391 return False
392
393
[docs]
394def is_rule_suboption_equal_to_regex(
395 rule: idstools.rule.Rule,
396 name: str,
397 sub_name: str,
398 regex, # re.Pattern or regex.Pattern # noqa: ANN001
399) -> bool:
400 """Checks whether a rule has a certain option set to match a certain regex.
401
402 If the option is set multiple times, it will return True if atleast one option matches the regex.
403
404 Args:
405 rule (idstools.rule.Rule): rule to be inspected
406 name (str): name of the option
407 sub_name (str): name of the suboption
408 regex (Union[re.Pattern, regex.Pattern]): regex to check for
409
410 Returns:
411 bool: True iff the rule has atleast one option matching the regex
412
413 """
414 if not is_rule_suboption_set(rule, name, sub_name):
415 return False
416
417 values = get_rule_suboptions(rule, name)
418
419 for key, value in values:
420 if key == sub_name and regex.match(value) is not None:
421 return True
422
423 return False
424
425
[docs]
426def is_rule_option_always_equal_to_regex(
427 rule: idstools.rule.Rule,
428 name: str,
429 regex, # re.Pattern or regex.Pattern # noqa: ANN001
430) -> Optional[bool]:
431 """Checks whether a rule has a certain option set to match a certain regex.
432
433 If the option is set multiple times, it will return True if all options match the regex.
434 Returns none if the rule option is not set.
435
436 Args:
437 rule (idstools.rule.Rule): rule to be inspected
438 name (str): name of the option
439 regex (Union[re.Pattern, regex.Pattern]): regex to check for
440
441 Returns:
442 bool: True iff the rule has all options matching the regex
443
444 """
445 if not is_rule_option_set(rule, name):
446 return None
447
448 values = get_rule_options(rule, name)
449
450 for value in values:
451 if value is None:
452 return False
453 if regex.match(value) is None:
454 return False
455
456 return True
457
458
[docs]
459def is_rule_suboption_always_equal_to_regex(
460 rule: idstools.rule.Rule,
461 name: str,
462 sub_name: str,
463 regex, # re.Pattern or regex.Pattern # noqa: ANN001
464) -> Optional[bool]:
465 """Checks whether a rule has a certain option set to match a certain regex.
466
467 If the option is set multiple times, it will return True if all options match the regex.
468 Returns none if the rule option is not set.
469
470 Args:
471 rule (idstools.rule.Rule): rule to be inspected
472 name (str): name of the option
473 sub_name (str): name of the suboption
474 regex (Union[re.Pattern, regex.Pattern]): regex to check for
475
476 Returns:
477 bool: True iff the rule has all options matching the regex
478
479 """
480 if not is_rule_suboption_set(rule, name, sub_name):
481 return None
482
483 values = get_rule_suboptions(rule, name)
484
485 for key, value in values:
486 if key == sub_name and regex.match(value) is None:
487 return False
488
489 return True
490
491
[docs]
492def are_rule_options_equal_to_regex(
493 rule: idstools.rule.Rule,
494 names: Iterable[str],
495 regex, # re.Pattern or regex.Pattern # noqa: ANN001
496) -> bool:
497 """Checks whether a rule has certain options set to match a certain regex.
498
499 If multiple options are set, it will return True if atleast one option matches the regex.
500
501 Args:
502 rule (idstools.rule.Rule): rule to be inspected
503 names (Iterable[str]): names of the options
504 regex (Union[re.Pattern, regex.Pattern]): regex to check for
505
506 Returns:
507 bool: True iff the rule has atleast one option matching the regex
508
509 """
510 for name in names:
511 if is_rule_option_equal_to_regex(rule, name, regex):
512 return True
513
514 return False
515
516
[docs]
517def is_rule_option_one_of(
518 rule: idstools.rule.Rule,
519 name: str,
520 possible_values: Union[Sequence[str], set[str]],
521) -> bool:
522 """Checks whether a rule has a certain option set to a one of certain values.
523
524 If the option is set multiple times, it will return True if atleast one option matches a value.
525
526 Args:
527 rule (idstools.rule.Rule): rule to be inspected
528 name (str): name of the option
529 possible_values (Iterable[str]): values to check for
530
531 Returns:
532 bool: True iff the rule has the option set to one of the values atleast once
533
534 """
535 if not is_rule_option_set(rule, name):
536 return False
537
538 values = get_rule_options(rule, name)
539
540 for value in values:
541 if value is None:
542 continue
543 if value in possible_values:
544 return True
545
546 return False
547
548
[docs]
549def get_rule_sticky_buffer_naming(rule: idstools.rule.Rule) -> list[tuple[str, str]]:
550 """Returns a list of tuples containing the name of a sticky buffer, and the modifier alternative."""
551 sticky_buffer_naming = []
552 for option in rule["options"]:
553 if option["name"] in STICKY_BUFFER_NAMING:
554 sticky_buffer_naming.append(
555 (option["name"], STICKY_BUFFER_NAMING[option["name"]]),
556 )
557
558 return sticky_buffer_naming
559
560
[docs]
561def get_all_variable_groups(rule: idstools.rule.Rule) -> list[str]:
562 """Returns a list of variable groups such as $HTTP_SERVERS in a rule."""
563 return __get_all_variable_groups(rule)
564
565
566@lru_cache(maxsize=_LRU_CACHE_SIZE)
567def __get_all_variable_groups(rule: idstools.rule.Rule) -> list[str]:
568 variable_groups = []
569 for name in (
570 "source_addr",
571 "source_port",
572 "direction",
573 "dest_addr",
574 "dest_port",
575 ):
576 if is_rule_option_set(rule, name):
577 value = get_rule_option(rule, name)
578 assert value is not None
579 variable_groups += get_variable_groups(value)
580
581 return variable_groups
582
583
[docs]
584def get_rule_option_positions(
585 rule: idstools.rule.Rule, name: str, sequence: Optional[tuple[str, ...]] = None
586) -> Sequence[int]:
587 """Finds the positions of an option in the rule body.
588
589 Optionally takes a sequence of options to use instead of `rule['options']`.
590 """
591 return __get_rule_option_positions(rule, name, sequence=sequence)
592
593
594@lru_cache(maxsize=_LRU_CACHE_SIZE)
595def __get_rule_option_positions(
596 rule: idstools.rule.Rule, name: str, sequence: Optional[tuple[str, ...]] = None
597) -> Sequence[int]:
598 provided_sequence = True
599 if sequence is None:
600 sequence = tuple(option["name"] for option in rule["options"])
601 provided_sequence = False
602
603 positions = []
604 for i, option in enumerate(sequence):
605 if option == name:
606 positions.append(i)
607
608 if not provided_sequence and len(positions) == 0 and is_rule_option_set(rule, name):
609 msg = f"Cannot determine position of {name} option since it is not part of the sequence of detection keywords."
610 _logger.critical(msg)
611 raise ValueError(msg)
612
613 return tuple(sorted(positions))
614
615
[docs]
616def get_rule_option_position(rule: idstools.rule.Rule, name: str) -> Optional[int]:
617 """Finds the position of an option in the rule body.
618
619 Return None if the option is not set or set multiple times.
620 """
621 return __get_rule_option_position(rule, name)
622
623
624@lru_cache(maxsize=_LRU_CACHE_SIZE)
625def __get_rule_option_position(rule: idstools.rule.Rule, name: str) -> Optional[int]:
626 positions = get_rule_option_positions(rule, name)
627
628 if len(positions) == 0:
629 _logger.debug(
630 "Cannot unambigously determine the position of the %s option since it it not set.",
631 name,
632 )
633 return None
634
635 if len(positions) == 1:
636 return positions[0]
637
638 _logger.debug(
639 "Cannot unambigously determine the position of the %s option since it is set multiple times.",
640 name,
641 )
642 return None
643
644
[docs]
645def is_rule_option_first(rule: idstools.rule.Rule, name: str) -> Optional[int]:
646 """Checks if a rule option is positioned at the beginning of the body."""
647 position = get_rule_option_position(rule, name)
648
649 if position is None:
650 _logger.debug("Cannot unambiguously determine if option %s first.", name)
651 return None
652
653 if position == 0:
654 return True
655
656 return False
657
658
[docs]
659def is_rule_option_last(rule: idstools.rule.Rule, name: str) -> Optional[bool]:
660 """Checks if a rule option is positioned at the end of the body."""
661 position = get_rule_option_position(rule, name)
662
663 if position is None:
664 _logger.debug("Cannot unambiguously determine if option %s last.", name)
665 return None
666
667 if position == len(rule["options"]) - 1:
668 return True
669
670 return False
671
672
[docs]
673def get_rule_options_positions(
674 rule: idstools.rule.Rule,
675 names: Iterable[str],
676 sequence: Optional[Iterable[str]] = None,
677) -> Iterable[int]:
678 """Finds the positions of several options in the rule body."""
679 return __get_rule_options_positions(
680 rule, tuple(sorted(names)), sequence=tuple(sequence) if sequence else None
681 )
682
683
684@lru_cache(maxsize=_LRU_CACHE_SIZE)
685def __get_rule_options_positions(
686 rule: idstools.rule.Rule,
687 names: Iterable[str],
688 sequence: Optional[tuple[str, ...]] = None,
689) -> Iterable[int]:
690 positions = []
691
692 for name in names:
693 positions.extend(get_rule_option_positions(rule, name, sequence=sequence))
694
695 return tuple(sorted(positions))
696
697
[docs]
698def is_rule_option_put_before(
699 rule: idstools.rule.Rule,
700 name: str,
701 other_names: Union[Sequence[str], set[str]],
702 sequence: Optional[Iterable[str]] = None,
703) -> Optional[bool]:
704 """Checks whether a rule option is placed before one or more other options."""
705 return __is_rule_option_put_before(
706 rule,
707 name,
708 tuple(sorted(other_names)),
709 sequence=tuple(sequence) if sequence else None,
710 )
711
712
713@lru_cache(maxsize=_LRU_CACHE_SIZE)
714def __is_rule_option_put_before(
715 rule: idstools.rule.Rule,
716 name: str,
717 other_names: Union[Sequence[str], set[str]],
718 sequence: Optional[tuple[str, ...]] = None,
719) -> Optional[bool]:
720 if len(other_names) == 0:
721 _logger.debug(
722 "Cannot unambiguously determine if option %s is put before empty Iterable of other options.",
723 name,
724 )
725 return None
726
727 positions = get_rule_option_positions(rule, name, sequence=sequence)
728
729 if name in other_names:
730 _logger.debug("Excluding name %s from other_names because of overlap.", name)
731 other_names = set(other_names).difference({name})
732
733 other_positions = get_rule_options_positions(rule, other_names, sequence=sequence)
734
735 for other_position in other_positions:
736 for position in positions:
737 if position < other_position:
738 return True
739 return False
740
741
[docs]
742def is_rule_option_always_put_before(
743 rule: idstools.rule.Rule,
744 name: str,
745 other_names: Union[Sequence[str], set[str]],
746 sequence: Optional[Iterable[str]] = None,
747) -> Optional[bool]:
748 """Checks whether a rule option is placed before one or more other options."""
749 return __is_rule_option_always_put_before(
750 rule,
751 name,
752 tuple(sorted(other_names)),
753 sequence=tuple(sequence) if sequence else None,
754 )
755
756
757@lru_cache(maxsize=_LRU_CACHE_SIZE)
758def __is_rule_option_always_put_before(
759 rule: idstools.rule.Rule,
760 name: str,
761 other_names: Union[Sequence[str], set[str]],
762 sequence: Optional[tuple[str, ...]] = None,
763) -> Optional[bool]:
764 if len(other_names) == 0:
765 _logger.debug(
766 "Cannot unambiguously determine if option %s is put before empty Iterable of other options.",
767 name,
768 )
769 return None
770
771 positions = get_rule_option_positions(rule, name, sequence=sequence)
772
773 if name in other_names:
774 _logger.debug("Excluding name %s from other_names because of overlap.", name)
775 other_names = set(other_names).difference({name})
776
777 other_positions = get_rule_options_positions(rule, other_names, sequence=sequence)
778
779 for other_position in other_positions:
780 for position in positions:
781 if position >= other_position:
782 return False
783 return True
784
785
[docs]
786def are_rule_options_put_before(
787 rule: idstools.rule.Rule,
788 names: Union[Sequence[str], set[str]],
789 other_names: Union[Sequence[str], set[str]],
790 sequence: Optional[Iterable[str]] = None,
791) -> Optional[bool]:
792 """Checks whether rule options are placed before one or more other options."""
793 if len(other_names) == 0:
794 _logger.debug(
795 "Cannot unambiguously determine if an empty Iterable of options are put before other options %s.",
796 other_names,
797 )
798 return None
799 if len(other_names) == 0:
800 _logger.debug(
801 "Cannot unambiguously determine if options %s are put before empty Iterable of other options.",
802 names,
803 )
804 return None
805
806 for name in names:
807 if is_rule_option_put_before(rule, name, other_names, sequence=sequence):
808 return True
809 return False
810
811
[docs]
812def are_rule_options_always_put_before(
813 rule: idstools.rule.Rule,
814 names: Iterable[str],
815 other_names: Sequence[str],
816 sequence: Optional[Iterable[str]] = None,
817) -> Optional[bool]:
818 """Checks whether rule options are placed before one or more other options."""
819 if len(other_names) == 0:
820 _logger.debug(
821 "Cannot unambiguously determine if an empty Iterable of options are put before other options %s.",
822 other_names,
823 )
824 return None
825 if len(other_names) == 0:
826 _logger.debug(
827 "Cannot unambiguously determine if options %s are put before empty Iterable of other options.",
828 names,
829 )
830 return None
831
832 for name in names:
833 if not is_rule_option_put_before(rule, name, other_names, sequence=sequence):
834 return False
835 return True
836
837
[docs]
838def select_rule_options_by_regex(
839 rule: idstools.rule.Rule, regex # noqa: ANN001
840) -> Iterable[str]:
841 """Selects rule options present in rule matching a regular expression."""
842 return __select_rule_options_by_regex(rule, regex)
843
844
845@lru_cache(maxsize=_LRU_CACHE_SIZE)
846def __select_rule_options_by_regex(
847 rule: idstools.rule.Rule, regex # noqa: ANN001
848) -> Iterable[str]:
849 options = []
850
851 for option in rule["options"]:
852 name = option["name"]
853 if _regex_provider.match(regex, name):
854 options.append(name)
855
856 return tuple(sorted(options))
857
858
[docs]
859def get_rule_keyword_sequences(
860 rule: idstools.rule.Rule,
861 seperator_keywords: Iterable[str] = BUFFER_KEYWORDS,
862 included_keywords: Iterable[str] = ALL_DETECTION_KEYWORDS,
863) -> Sequence[tuple[str, ...]]:
864 """Returns a sequence of sequences of detection options in a rule."""
865 sequences: list[list[str]] = []
866
867 # Relies on the assumption that the order of options in the rule is preserved while parsing
868 sequence_i = -1
869 first_seperator_seen = False
870 for option in rule["options"]:
871 name = option["name"]
872 if name in seperator_keywords:
873 if not first_seperator_seen:
874 if len(sequences) > 0:
875 sequences[sequence_i].append(name)
876 else:
877 sequence_i += 1
878 sequences.append([name])
879 else:
880 sequence_i += 1
881 sequences.append([name])
882 first_seperator_seen = True
883 elif name in included_keywords and sequence_i == -1:
884 sequence_i += 1
885 sequences.append([name])
886 elif name in included_keywords:
887 sequences[sequence_i].append(name)
888
889 if len(sequences) == 0:
890 _logger.debug(
891 "No sequences found separated by %s in rule %s",
892 seperator_keywords,
893 rule["raw"],
894 )
895 return ()
896
897 for sequence in sequences:
898 assert len(sequence) > 0
899
900 result = tuple(tuple(sequence) for sequence in sequences)
901
902 _logger.debug(
903 "Detected sequences %s separated by %s in rule %s",
904 result,
905 seperator_keywords,
906 rule["raw"],
907 )
908
909 return result