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