From 72983c2efbc7cd821d4384bd5ecdf78fa37d0415 Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Sat, 10 Aug 2024 02:50:20 +0200 Subject: [PATCH] Validator parameter configuration Moved validator check arguments to class construction. --- docs/Rule_Validation.rst | 24 +++++++++++++++++++ sigma/validation.py | 24 +++++++++++++++---- sigma/validators/base.py | 2 +- sigma/validators/core/metadata.py | 21 ++++++++++------ tests/test_validation.py | 19 +++++++++++++++ tests/test_validators_metadata.py | 40 ++++++++++++++++++++++++------- 6 files changed, 109 insertions(+), 21 deletions(-) diff --git a/docs/Rule_Validation.rst b/docs/Rule_Validation.rst index de8947d7..41a8916c 100644 --- a/docs/Rule_Validation.rst +++ b/docs/Rule_Validation.rst @@ -107,6 +107,19 @@ applied to the rule. Example: This exclusion defines that the *wildcards_instead_of_modifiers* validator check is disabled for the rule with the identifier *5013332f-8a70-4e04-bcc1-06a98a2cca2e*. +Configuration +------------- + +Validator checks that accept parameters can be configured with a dictionary that is passed as the +*config* parameter. This dictionary maps validator identifiers to dictionaries of parameter-value +pairs that are passed as keyword arguments to the validator constructor. Example: + +.. code-block:: yaml + + config: + description_length: + min_length: 100 + Validator Checks **************** @@ -148,6 +161,17 @@ desired rule part and takes care of the proper iteration of these parts. These c * :py:class:`sigma.validators.base.SigmaTagValueValidator` for checking all tags appearing beloe the *tags* attribute of a Sigma rule. +Parametrization of Checks +========================= + +If required, checks can be parametrized by passing parameters as keyword arguments to the validator +check constructor. for this purpose, the validator check class must be a *frozen dataclass*. This +can be achieved by decorating the class with `@dataclass(frozen=True)` from the *dataclasses* +module. + +The parameters can then be specified as dataclass members. The `SigmaValidator` instance will pass +the parameters to the validator check constructor as keyword arguments. + Base Classes ============ diff --git a/sigma/validation.py b/sigma/validation.py index 470e622d..55a71162 100644 --- a/sigma/validation.py +++ b/sigma/validation.py @@ -1,7 +1,6 @@ from collections import defaultdict -from typing import Callable, DefaultDict, Dict, Iterable, Iterator, List, Set, Type +from typing import DefaultDict, Dict, Iterable, Iterator, List, Set, Type, Union from uuid import UUID -from sigma.collection import SigmaCollection from sigma.exceptions import SigmaConfigurationError from sigma.rule import SigmaRule from sigma.validators.base import SigmaRuleValidator, SigmaValidationIssue @@ -20,13 +19,17 @@ class SigmaValidator: validators: Set[SigmaRuleValidator] exclusions: DefaultDict[UUID, Set[Type[SigmaRuleValidator]]] + config: Dict[str, Dict[str, Union[str, int, float, bool]]] def __init__( self, validators: Iterable[Type[SigmaRuleValidator]], exclusions: Dict[UUID, Set[SigmaRuleValidator]] = dict(), + config: Dict[str, Dict[str, Union[str, int, float, bool]]] = dict(), ): - self.validators = {validator() for validator in validators} + self.validators = { + validator(**config.get(validator.__name__, {})) for validator in validators + } self.exclusions = defaultdict(set, exclusions) @classmethod @@ -39,6 +42,8 @@ def from_dict(cls, d: Dict, validators: Dict[str, SigmaRuleValidator]) -> "Sigma represents all known validators. * exclusion: a map between rule ids and lists of validator names or a single validator name to define validation exclusions. + * config: a map between validator names and configuration dicts that are passed as + keyword arguments to the validator constructor. :param d: Definition of the SigmaValidator. :type d: Dict @@ -84,7 +89,18 @@ def from_dict(cls, d: Dict, validators: Dict[str, SigmaRuleValidator]) -> "Sigma except KeyError as e: raise SigmaConfigurationError(f"Unknown validator '{ e.args[0] }'") - return cls(validator_classes, exclusions) + # Build configuration dict + configuration = dict() + for validator_name, params in d.get("config", {}).items(): + if validator_name not in validators: + raise SigmaConfigurationError(f"Unknown validator '{ validator_name }'") + if not isinstance(params, dict): + raise SigmaConfigurationError( + f"Configuration for validator '{ validator_name }' is not a dict." + ) + configuration[validators[validator_name].__name__] = params + + return cls(validator_classes, exclusions, configuration) @classmethod def from_yaml( diff --git a/sigma/validators/base.py b/sigma/validators/base.py index f5c44b42..6afabd69 100644 --- a/sigma/validators/base.py +++ b/sigma/validators/base.py @@ -71,7 +71,7 @@ class SigmaRuleValidator(ABC): """ @abstractmethod - def validate(self, rule: SigmaRuleBase, **kargs) -> List[SigmaValidationIssue]: + def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]: """Implementation of the rule validation. :param rule: Sigma rule that should be validated. diff --git a/sigma/validators/core/metadata.py b/sigma/validators/core/metadata.py index 61283962..a814f3df 100644 --- a/sigma/validators/core/metadata.py +++ b/sigma/validators/core/metadata.py @@ -193,20 +193,24 @@ def finalize(self) -> List[SigmaValidationIssue]: @dataclass -class FilenameLenghIssue(SigmaValidationIssue): +class FilenameLengthIssue(SigmaValidationIssue): description: ClassVar[str] = "Rule filename is too short or long" severity: ClassVar[SigmaValidationIssueSeverity] = SigmaValidationIssueSeverity.HIGH filename: str -class FilenameLenghValidator(SigmaRuleValidator): +@dataclass(frozen=True) +class FilenameLengthValidator(SigmaRuleValidator): """Check rule filename lengh""" - def validate(self, rule: SigmaRule, minsize=10, maxsize=90) -> List[SigmaValidationIssue]: + min_size: int = 10 + max_size: int = 90 + + def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]: if rule.source is not None: filename = rule.source.path.name - if len(filename) < minsize or len(filename) > maxsize: - return [FilenameLenghIssue(rule, filename)] + if len(filename) < self.min_size or len(filename) > self.max_size: + return [FilenameLengthIssue(rule, filename)] return [] @@ -258,11 +262,14 @@ class DescriptionLengthIssue(SigmaValidationIssue): severity: ClassVar[SigmaValidationIssueSeverity] = SigmaValidationIssueSeverity.MEDIUM +@dataclass(frozen=True) class DescriptionLengthValidator(SigmaRuleValidator): """Checks if rule has a description.""" - def validate(self, rule: SigmaRule, minlength=16) -> List[SigmaValidationIssue]: - if rule.description is not None and len(rule.description) < minlength: + min_length: int = 16 + + def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]: + if rule.description is not None and len(rule.description) < self.min_length: return [DescriptionLengthIssue([rule])] else: return [] diff --git a/tests/test_validation.py b/tests/test_validation.py index 3ffd69d3..dadf924e 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,3 +1,4 @@ +import dataclasses from uuid import UUID import pytest from sigma.exceptions import SigmaConfigurationError @@ -9,6 +10,7 @@ from tests.test_validators import rule_with_id, rule_without_id, rules_with_id_collision from sigma.collection import SigmaCollection from sigma.validators.core.metadata import ( + DescriptionLengthValidator, IdentifierExistenceValidator, IdentifierUniquenessValidator, IdentifierExistenceIssue, @@ -22,6 +24,13 @@ def validators(): return InstalledSigmaPlugins.autodiscover().validators +@pytest.mark.parametrize("validator", InstalledSigmaPlugins.autodiscover().validators.values()) +def test_parametrized_validators_are_frozen(validator): + assert not dataclasses.is_dataclass(validator) or ( + dataclasses.is_dataclass(validator) and validator.__dataclass_params__.frozen + ) + + def test_sigmavalidator_validate_rules(rule_with_id, rule_without_id, rules_with_id_collision): rules = SigmaCollection([rule_with_id, rule_without_id, *rules_with_id_collision]) validator = SigmaValidator({IdentifierExistenceValidator, IdentifierUniquenessValidator}) @@ -72,11 +81,17 @@ def test_sigmavalidator_from_dict(validators): "number_as_string", ], }, + "config": { + "description_length": { + "min_length": 100, + }, + }, }, validators, ) assert DanglingDetectionValidator in (v.__class__ for v in validator.validators) assert TLPv1TagValidator not in (v.__class__ for v in validator.validators) + assert DescriptionLengthValidator(min_length=100) in validator.validators assert len(validator.validators) >= 10 assert validator.exclusions == { UUID("c702c6c7-1393-40e5-93f8-91469f3445ad"): {DanglingDetectionValidator}, @@ -99,11 +114,15 @@ def test_sigmavalidator_from_yaml(validators): bf39335e-e666-4eaf-9416-47f1955b5fb3: - attacktag - number_as_string + config: + description_length: + min_length: 100 """, validators, ) assert DanglingDetectionValidator in (v.__class__ for v in validator.validators) assert TLPv1TagValidator not in (v.__class__ for v in validator.validators) + assert DescriptionLengthValidator(min_length=100) in validator.validators assert len(validator.validators) >= 10 assert validator.exclusions == { UUID("c702c6c7-1393-40e5-93f8-91469f3445ad"): {DanglingDetectionValidator}, diff --git a/tests/test_validators_metadata.py b/tests/test_validators_metadata.py index a164b936..4ad710ff 100644 --- a/tests/test_validators_metadata.py +++ b/tests/test_validators_metadata.py @@ -1,9 +1,7 @@ from uuid import UUID -from wsgiref.validate import validator import pytest from sigma.rule import SigmaRule -from sigma.types import SigmaString from sigma.collection import SigmaCollection from sigma.validators.core.metadata import ( @@ -23,8 +21,8 @@ DateExistenceIssue, DuplicateFilenameValidator, DuplicateFilenameIssue, - FilenameLenghValidator, - FilenameLenghIssue, + FilenameLengthValidator, + FilenameLengthIssue, CustomAttributesValidator, CustomAttributesIssue, DescriptionExistenceValidator, @@ -324,15 +322,22 @@ def test_validator_duplicate_filename_multiple_rules_in_one_file(): assert validator.finalize() == [] -def test_validator_filename_lengh(): - validator = FilenameLenghValidator() +def test_validator_filename_length(): + validator = FilenameLengthValidator() sigma_collection = SigmaCollection.load_ruleset(["tests/files/rule_filename_errors"]) rule = sigma_collection[0] - assert validator.validate(rule) == [FilenameLenghIssue([rule], "Name.yml")] + assert validator.validate(rule) == [FilenameLengthIssue([rule], "Name.yml")] -def test_validator_filename_lengh_valid(): - validator = FilenameLenghValidator() +def test_validator_filename_length_customized_valid(): + validator = FilenameLengthValidator(min_size=0, max_size=999) + sigma_collection = SigmaCollection.load_ruleset(["tests/files/rule_filename_errors"]) + rule = sigma_collection[0] + assert validator.validate(rule) == [] + + +def test_validator_filename_length_valid(): + validator = FilenameLengthValidator() sigma_collection = SigmaCollection.load_ruleset(["tests/files/rule_valid"]) rule = sigma_collection[0] assert validator.validate(rule) == [] @@ -440,6 +445,23 @@ def test_validator_description_length_valid(): assert validator.validate(rule) == [] +def test_validator_description_length_valid_customized(): + validator = DescriptionLengthValidator(min_length=999) + rule = SigmaRule.from_yaml( + """ + title: Test + description: it is a simple description + logsource: + category: test + detection: + sel: + field: value + condition: sel + """ + ) + assert validator.validate(rule) == [DescriptionLengthIssue([rule])] + + def test_validator_level_existence(): validator = LevelExistenceValidator() rule = SigmaRule.from_yaml(