Source code for suricata_check.utils.checker

  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