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