diff --git a/sigma/rule/attributes.py b/sigma/rule/attributes.py index f39e0355..bb27c172 100644 --- a/sigma/rule/attributes.py +++ b/sigma/rule/attributes.py @@ -152,6 +152,10 @@ def from_dict(cls, val: list[dict[str, str]]) -> "SigmaRelated": list_ret: list[SigmaRelatedItem] = [] for v in val: + if not isinstance(v, dict): + raise sigma_exceptions.SigmaRelatedError( + "Sigma related items must be maps with an id and type field" + ) if "id" not in v.keys(): raise sigma_exceptions.SigmaRelatedError("Sigma related must have an id field") elif "type" not in v.keys(): diff --git a/sigma/rule/base.py b/sigma/rule/base.py index 5f25d67c..fa3228c4 100644 --- a/sigma/rule/base.py +++ b/sigma/rule/base.py @@ -208,14 +208,21 @@ def get_rule_as_date(name: str, exception_class: type[SigmaError]) -> date | Non # Rule level validation rule_level = rule.get("level") if rule_level is not None: - try: - rule_level = SigmaLevel[rule_level.upper()] - except KeyError: + if not isinstance(rule_level, str): errors.append( sigma_exceptions.SigmaLevelError( - f"'{ rule_level }' is not a valid Sigma rule level", source=source + "Sigma rule level must be a string", source=source ) ) + else: + try: + rule_level = SigmaLevel[rule_level.upper()] + except KeyError: + errors.append( + sigma_exceptions.SigmaLevelError( + f"'{ rule_level }' is not a valid Sigma rule level", source=source + ) + ) # Rule status validation rule_status = rule.get("status") @@ -248,6 +255,13 @@ def get_rule_as_date(name: str, exception_class: type[SigmaError]) -> date | Non ) else: for tag in tags: + if not isinstance(tag, str): + errors.append( + sigma_exceptions.SigmaTagError( + "Sigma rule tags must be a list of strings", source=source + ) + ) + continue try: rule_tags.append(SigmaRuleTag.from_str(tag)) except sigma_exceptions.SigmaValueError as e: diff --git a/tests/test_rule.py b/tests/test_rule.py index 63e8271b..99784908 100644 --- a/tests/test_rule.py +++ b/tests/test_rule.py @@ -1276,6 +1276,74 @@ def test_sigmarule_collect_errors(): } +@pytest.mark.parametrize( + ("rule", "expected_error"), + [ + ( + """ + title: Test + level: 123 + logsource: + product: windows + detection: + selection_1: + fieldA: valueA + condition: selection_1 + """, + sigma_exceptions.SigmaLevelError, + ), + ( + """ + title: Test + tags: + - 123 + logsource: + product: windows + detection: + selection_1: + fieldA: valueA + condition: selection_1 + """, + sigma_exceptions.SigmaTagError, + ), + ( + """ + title: Test + related: + - foo + logsource: + product: windows + detection: + selection_1: + fieldA: valueA + condition: selection_1 + """, + sigma_exceptions.SigmaRelatedError, + ), + ], +) +def test_sigmarule_meta_field_type_errors_collected(rule, expected_error): + """Non-string level, non-string tag and non-map related items must be reported + as validation errors, not crash with an AttributeError.""" + parsed = SigmaRule.from_yaml(rule, collect_errors=True) + assert expected_error in {error.__class__ for error in parsed.errors} + + +def test_sigmarule_meta_field_type_errors_raised(): + """Without collect_errors the first recognized error is raised as a SigmaError.""" + with pytest.raises(sigma_exceptions.SigmaLevelError): + SigmaRule.from_yaml(""" + title: Test + level: 123 + logsource: + product: windows + detection: + selection_1: + fieldA: valueA + condition: selection_1 + """) + + def test_sigmarule_no_logsource(): with pytest.raises( sigma_exceptions.SigmaLogsourceError, match="must have a log source.*test.yml"