diff --git a/tests/conftest.py b/tests/conftest.py index dc40e52d..aa0de784 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -106,6 +106,7 @@ def invalid_param_rule_data() -> Dict[str, Any]: const.PROFILE: { const.DESCRIPTION: "Simple NIST Profile", const.HREF: "profiles/simplified_nist_profile/profile.json", + const.INCLUDE_CONTROLS: [{"id": "ac-1"}], }, const.PARAMETER: { const.NAME: "prm_1", @@ -118,7 +119,12 @@ def invalid_param_rule_data() -> Dict[str, Any]: }, const.DEFAULT_VALUE: "5%", }, - } + }, + const.COMPONENT_INFO_TAG: { + const.NAME: "Component 1", + const.TYPE: "service", + const.DESCRIPTION: "Component 1 description", + }, } @@ -156,7 +162,7 @@ def test_rule() -> TrestleRule: parameter=Parameter( name="test", description="test", - alternative_values={}, + alternative_values={"default": "test", "test": "test"}, default_value="test", ), check=Check(name="test_check", description="test check"), diff --git a/tests/data/yaml/test_complete_rule.yaml b/tests/data/yaml/test_complete_rule.yaml index c17d77bf..07557190 100644 --- a/tests/data/yaml/test_complete_rule.yaml +++ b/tests/data/yaml/test_complete_rule.yaml @@ -4,8 +4,13 @@ x-trestle-rule-info: parameter: name: prm_1 description: prm_1 description - alternative-values: {'default': '5%', '5pc': '5%', '10pc': '10%', '15pc': '15%', '20pc': '20%'} - default-value: '5%' + alternative-values: + default: 5% + 5pc: 5% + 10pc: 10% + 15pc: 15% + 20pc: 20% + default-value: 5% check: name: my_check description: My check description diff --git a/tests/data/yaml/test_complete_rule_multiple_controls.yaml b/tests/data/yaml/test_complete_rule_multiple_controls.yaml index 022e1d74..8660a716 100644 --- a/tests/data/yaml/test_complete_rule_multiple_controls.yaml +++ b/tests/data/yaml/test_complete_rule_multiple_controls.yaml @@ -4,8 +4,12 @@ x-trestle-rule-info: parameter: name: prm_1 description: prm_1 description - alternative-values: {'default': '5%', '5pc': '5%', '10pc': '10%', '15pc': '15%', '20pc': '20%'} - default-value: '5%' + alternative-values: + default: 5% + 5pc: 5% + 10pc: 10% + 20pc: 20% + default-value: 5% check: name: my_check description: My check description diff --git a/tests/data/yaml/test_incomplete_rule.yaml b/tests/data/yaml/test_incomplete_rule.yaml index e8e2ec33..07caa99c 100644 --- a/tests/data/yaml/test_incomplete_rule.yaml +++ b/tests/data/yaml/test_incomplete_rule.yaml @@ -3,8 +3,12 @@ x-trestle-rule-info: parameter: name: prm_1 description: prm_1 description - alternative-values: {'default': '5%', '5pc': '5%', '10pc': '10%', '15pc': '15%', '20pc': '20%'} - default-value: '5%' + alternative-values: + default: 5% + 5pc: 5% + 10pc: 10% + 20pc: 20% + default-value: 5% profile: description: Simple NIST Profile href: profiles/simplified_nist_profile/profile.json diff --git a/tests/data/yaml/test_rule_invalid_params.yaml b/tests/data/yaml/test_rule_invalid_params.yaml index 3b4acdcd..900de236 100644 --- a/tests/data/yaml/test_rule_invalid_params.yaml +++ b/tests/data/yaml/test_rule_invalid_params.yaml @@ -4,10 +4,12 @@ x-trestle-rule-info: parameter: name: prm_1 description: prm_1 description - alternative-values: {'5pc': '5%', '10pc': '10%', '15pc': '15%', '20pc': '20%'} - default-value: '5%' + alternative-values: + default: 10% + 10pc: 10% + 20pc: 20% + default-value: 5% profile: - description: Simple NIST Profile href: profiles/simplified_nist_profile/profile.json include-controls: - id: ac-2 diff --git a/tests/trestlebot/tasks/test_rule_transform_task.py b/tests/trestlebot/tasks/test_rule_transform_task.py index cb8b7a37..b3557712 100644 --- a/tests/trestlebot/tasks/test_rule_transform_task.py +++ b/tests/trestlebot/tasks/test_rule_transform_task.py @@ -97,9 +97,7 @@ def test_rule_transform_task_with_invalid_rule(tmp_trestle_dir: str) -> None: tmp_trestle_dir, test_rules_dir, transformer ) - with pytest.raises( - TaskException, match="Failed to transform rule .*: Missing key in YAML file: .*" - ): + with pytest.raises(TaskException, match=".*: Missing key in YAML file: .*"): rule_transform_task.execute() diff --git a/tests/trestlebot/transformers/test_csv_transformer.py b/tests/trestlebot/transformers/test_csv_transformer.py index c9136eb6..4548e185 100644 --- a/tests/trestlebot/transformers/test_csv_transformer.py +++ b/tests/trestlebot/transformers/test_csv_transformer.py @@ -34,7 +34,7 @@ def test_csv_builder(test_rule: TrestleRule, tmp_trestle_dir: str) -> None: assert row["Control_Id_List"] == "ac-1 ac-2" assert row["Parameter_Id"] == test_rule.parameter.name # type: ignore assert row["Parameter_Description"] == test_rule.parameter.description # type: ignore - assert row["Parameter_Value_Alternatives"] == "{}" + assert row["Parameter_Value_Alternatives"] == '{"default": "test", "test": "test"}' assert row["Parameter_Value_Default"] == test_rule.parameter.default_value # type: ignore assert row["Profile_Description"] == test_rule.profile.description assert row["Profile_Source"] == test_rule.profile.href diff --git a/tests/trestlebot/transformers/test_validations.py b/tests/trestlebot/transformers/test_validations.py deleted file mode 100644 index 83eef03d..00000000 --- a/tests/trestlebot/transformers/test_validations.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2023 Red Hat, Inc. - - -"""Test for Validations.""" -from typing import Any, Dict - -from trestlebot.transformers.validations import ( - ValidationHandler, - ValidationOutcome, - parameter_validation, -) - - -def test_parameter_validation(valid_rule_data: Dict[str, Any]) -> None: - """Test parameter validation with valid data.""" - result: ValidationOutcome = ValidationOutcome(errors=[], valid=True) - parameter_validation(valid_rule_data, result) - assert result.valid - - -def test_parameter_validation_with_error( - invalid_param_rule_data: Dict[str, Any] -) -> None: - """Test parameter validation with invalid parameter.""" - result: ValidationOutcome = ValidationOutcome(errors=[], valid=True) - parameter_validation(invalid_param_rule_data, result) - assert not result.valid - assert len(result.errors) == 1 - assert ( - result.errors[0].error_message - == "Default value must be one of the alternative values" - ) - - -def test_parameter_validation_with_handler( - invalid_param_rule_data: Dict[str, Any] -) -> None: - """Test parameter validation with handler.""" - result: ValidationOutcome = ValidationOutcome(errors=[], valid=True) - handler = ValidationHandler(parameter_validation) - handler.handle(invalid_param_rule_data, result) - assert not result.valid - assert len(result.errors) == 1 - assert ( - result.errors[0].error_message - == "Default value must be one of the alternative values" - ) diff --git a/tests/trestlebot/transformers/test_yaml_transformer.py b/tests/trestlebot/transformers/test_yaml_transformer.py index 343e6df8..0995a818 100644 --- a/tests/trestlebot/transformers/test_yaml_transformer.py +++ b/tests/trestlebot/transformers/test_yaml_transformer.py @@ -4,12 +4,15 @@ """Test for YAML Transformer.""" +import json +import re +from typing import Any, Dict + import pytest from tests.testutils import YAML_TEST_DATA_PATH from trestlebot.transformers.base_transformer import RulesTransformerException from trestlebot.transformers.trestle_rule import TrestleRule -from trestlebot.transformers.validations import ValidationHandler, parameter_validation from trestlebot.transformers.yaml_transformer import ( FromRulesYAMLTransformer, ToRulesYAMLTransformer, @@ -21,9 +24,9 @@ def test_rule_transformer() -> None: # load rule from path and close the file # get the file info as a string rule_path = YAML_TEST_DATA_PATH / "test_complete_rule.yaml" - rule_file = open(rule_path, "r") - rule_file_info = rule_file.read() - rule_file.close() + rule_file_info: str + with open(rule_path, "r") as rule_file: + rule_file_info = rule_file.read() transformer = ToRulesYAMLTransformer() rule = transformer.transform(rule_file_info) @@ -69,31 +72,47 @@ def test_rules_transform_with_invalid_rule() -> None: # load rule from path and close the file # get the file info as a string rule_path = YAML_TEST_DATA_PATH / "test_invalid_rule.yaml" - rule_file = open(rule_path, "r") - rule_file_info = rule_file.read() - rule_file.close() + rule_file_info: str + with open(rule_path, "r") as rule_file: + rule_file_info = rule_file.read() transformer = ToRulesYAMLTransformer() with pytest.raises( - RulesTransformerException, match="Invalid YAML file: 1 validation error .*" + RulesTransformerException, match=".*value is not a valid dict.*" ): transformer.transform(rule_file_info) +def test_rules_without_default(invalid_param_rule_data: Dict[str, Any]) -> None: + """Test rules without default parameter value.""" + transformer = ToRulesYAMLTransformer() + + json_str = json.dumps(invalid_param_rule_data) + rule = transformer.transform(json_str) + assert "default" in rule.parameter.alternative_values # type: ignore + assert ( + rule.parameter.alternative_values.get("default") == rule.parameter.default_value # type: ignore + ) + + def test_rules_transform_with_additional_validation() -> None: """Test rules transform with additional validation.""" # load rule from path and close the file # get the file info as a string rule_path = YAML_TEST_DATA_PATH / "test_rule_invalid_params.yaml" - rule_file = open(rule_path, "r") - rule_file_info = rule_file.read() - rule_file.close() - validation_handler_chain = ValidationHandler(parameter_validation) - transformer = ToRulesYAMLTransformer(validation_handler_chain) + rule_file_info: str + with open(rule_path, "r") as rule_file: + rule_file_info = rule_file.read() + transformer = ToRulesYAMLTransformer() + + expected_error = """2 error(s) found: +Location: description, Type: value_error.missing, Message: field required +Location: default-value, Type: value_error, Message: Default value 5% must be in the alternative \ +values dict_values(['10%', '10%', '20%'])""" with pytest.raises( RulesTransformerException, - match=".*Default value must be one of the alternative values", + match=re.escape(expected_error), ): transformer.transform(rule_file_info) diff --git a/trestlebot/const.py b/trestlebot/const.py index ac9143d4..76560a8c 100644 --- a/trestlebot/const.py +++ b/trestlebot/const.py @@ -29,7 +29,10 @@ PROFILE = "profile" HREF = "href" ALTERNATIVE_VALUES = "alternative-values" +DEFAULT_KEY = "default" DEFAULT_VALUE = "default-value" +TYPE = "type" +INCLUDE_CONTROLS = "include-controls" COMPONENT_YAML = "component.yaml" COMPONENT_INFO_TAG = trestle_const.TRESTLE_TAG + "component-info" diff --git a/trestlebot/entrypoints/entrypoint_base.py b/trestlebot/entrypoints/entrypoint_base.py index 5c73da03..c9b3cce4 100644 --- a/trestlebot/entrypoints/entrypoint_base.py +++ b/trestlebot/entrypoints/entrypoint_base.py @@ -215,7 +215,7 @@ def handle_exception( exception: Exception, msg: str = "Exception occurred during execution" ) -> int: """Log the exception and return the exit code""" - logger.error(msg + f": {exception}", exc_info=True) + logger.error(msg + f": {exception}") if isinstance(exception, EntrypointInvalidArgException): return const.INVALID_ARGS_EXIT_CODE diff --git a/trestlebot/entrypoints/rule_transform.py b/trestlebot/entrypoints/rule_transform.py index 53cf30b5..d3a71339 100644 --- a/trestlebot/entrypoints/rule_transform.py +++ b/trestlebot/entrypoints/rule_transform.py @@ -18,7 +18,6 @@ from trestlebot.entrypoints.log import set_log_level_from_args from trestlebot.tasks.base_task import ModelFilter, TaskBase from trestlebot.tasks.rule_transform_task import RuleTransformTask -from trestlebot.transformers.validations import ValidationHandler, parameter_validation from trestlebot.transformers.yaml_transformer import ToRulesYAMLTransformer @@ -56,20 +55,13 @@ def run(self, args: argparse.Namespace) -> None: try: set_log_level_from_args(args) - # Configure the YAML Transformer for the task - validation_handler: ValidationHandler = ValidationHandler( - parameter_validation - ) - transformer: ToRulesYAMLTransformer = ToRulesYAMLTransformer( - validation_handler - ) - # Allow any model to be skipped from the args, by default include all model_filter: ModelFilter = ModelFilter( skip_patterns=comma_sep_to_list(args.skip_items), include_patterns=["*"], ) + transformer = ToRulesYAMLTransformer() rule_transform_task: RuleTransformTask = RuleTransformTask( working_dir=args.working_dir, rules_view_dir=args.rules_view_path, diff --git a/trestlebot/tasks/rule_transform_task.py b/trestlebot/tasks/rule_transform_task.py index 3f25d372..52f59339 100644 --- a/trestlebot/tasks/rule_transform_task.py +++ b/trestlebot/tasks/rule_transform_task.py @@ -96,16 +96,14 @@ def _transform_components(self, component_definition_path: pathlib.Path) -> None rule = self._rule_transformer.transform(rule_stream) csv_builder.add_row(rule) except RulesTransformerException as e: - transformation_errors.append( - f"Failed to transform rule {rule_path.name}: {e}" - ) + transformation_errors.append(f"{rule_path.as_posix()}: {e}") if len(transformation_errors) > 0: + transformation_error_str = "\n".join(transformation_errors) raise TaskException( f"Failed to transform rules for component definition {component_definition_path.name}: \ - \n{', '.join(transformation_errors)}" + {transformation_error_str}" ) - if csv_builder.row_count == 0: raise TaskException( f"No rules found for component definition {component_definition_path.name}" diff --git a/trestlebot/transformers/csv_transformer.py b/trestlebot/transformers/csv_transformer.py index d7ca5c9f..978b6fe2 100644 --- a/trestlebot/transformers/csv_transformer.py +++ b/trestlebot/transformers/csv_transformer.py @@ -179,7 +179,7 @@ def _add_parameter(self, parameter: Parameter) -> Dict[str, str]: parameter_dict: Dict[str, str] = { PARAMETER_ID: parameter.name, PARAMETER_DESCRIPTION: parameter.description, - PARAMETER_VALUE_ALTERNATIVES: f"{parameter.alternative_values}", + PARAMETER_VALUE_ALTERNATIVES: json.dumps(parameter.alternative_values), PARAMETER_VALUE_DEFAULT: parameter.default_value, } return parameter_dict diff --git a/trestlebot/transformers/trestle_rule.py b/trestlebot/transformers/trestle_rule.py index 80752e59..cda67614 100644 --- a/trestlebot/transformers/trestle_rule.py +++ b/trestlebot/transformers/trestle_rule.py @@ -2,33 +2,71 @@ # Copyright (c) 2023 Red Hat, Inc. -"""Trestle Rule class with pydantic.""" +""" +Trestle Rule class with pydantic. -from typing import Any, Dict, List, Optional +Note: Any validation here should be done in the pydantic model and +required for the rule to be valid. +""" -from pydantic import BaseModel, Field + +from typing import Any, Dict, List, Optional, Tuple, Union + +from pydantic import BaseModel, Field, ValidationError, validator + +from trestlebot import const class Parameter(BaseModel): - """Parameter dataclass.""" + """Rule parameter model""" name: str description: str - alternative_values: Dict[str, Any] = Field(..., alias="alternative-values") + alternative_values: Dict[str, str] = Field(..., alias="alternative-values") default_value: str = Field(..., alias="default-value") class Config: allow_population_by_field_name = True + @validator("default_value", pre=False) + def check_default_value(cls, value: str, values: Dict[str, Any]) -> str: + """Check if default value is in the alternative values.""" + alternative_values: Dict[str, str] = values.get("alternative_values", {}) + if not alternative_values: + raise ValueError("Alternative values must be provided") + + if value not in alternative_values.values(): + raise ValueError( + f"Default value {value} must be in the alternative values {alternative_values.values()}" + ) + + # This is required to be listed as a value with a descriptive key and + # optionally the default key + default_value_alt = alternative_values.get(const.DEFAULT_KEY, "") + if not default_value_alt: + alternative_values[const.DEFAULT_KEY] = value + else: + if default_value_alt != value: + raise ValueError( + f"Default value {value} must be in the alternative values {alternative_values}" + f" under the key {const.DEFAULT_KEY}" + ) + + return value + class Control(BaseModel): - """Control dataclass.""" + """ + Catalog control for rule association + + Note: This can be the control id or statement id. + """ id: str class Profile(BaseModel): - """Profile dataclass.""" + """Profile source for rule association.""" description: str href: str @@ -39,7 +77,7 @@ class Config: class ComponentInfo(BaseModel): - """ComponentInfo dataclass.""" + """Rule component model.""" name: str type: str @@ -47,7 +85,7 @@ class ComponentInfo(BaseModel): class Check(BaseModel): - """Check dataclass.""" + """Check model for rule validation.""" name: str description: str @@ -57,7 +95,7 @@ class Config: class TrestleRule(BaseModel): - """TrestleRule dataclass.""" + """Represents a Trestle rule.""" name: str description: str @@ -83,3 +121,47 @@ def get_default_rule() -> TrestleRule: include_controls=[Control(id="example")], ), ) + + +# Adapted from https://docs.pydantic.dev/latest/errors/errors/ +def location_to_dot_separation( + location: Tuple[Union[str, int], ...] +) -> str: # pragma: no cover + """Convert a tuple of strings and integers to a dot separated string.""" + path: str = "" + for i, loc_value in enumerate(location): + if isinstance(loc_value, str): + if i > 0: + path += "." + path += loc_value + elif isinstance(loc_value, int): + path += f"[{loc_value}]" + else: + raise TypeError(f"Unexpected type {loc_value} in location tuple") + return path + + +def convert_errors(errors: List[ValidationError]) -> str: + """ + Convert pydantic validation errors into a formatted string. + + Note: All validations for rules should be done in the pydantic model and + formatted through this function is for display purposes. + """ + error_count: int = 0 + formatted_errors: List[str] = [] + + for validation_error in errors: + validation_errors = validation_error.errors() + error_count += len(validation_errors) + + for error in validation_errors: + location = location_to_dot_separation(error.get("loc")) + msg = error.get("msg") + typ = error.get("type") + formatted_errors.append( + f"Location: {location}, Type: {typ}, Message: {msg}" + ) + + pretty_errors = "\n".join(formatted_errors) + return f"{error_count} error(s) found:\n{pretty_errors}" diff --git a/trestlebot/transformers/validations.py b/trestlebot/transformers/validations.py deleted file mode 100644 index 6d45ec02..00000000 --- a/trestlebot/transformers/validations.py +++ /dev/null @@ -1,84 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2023 Red Hat, Inc. - - -""" -Trestle Validation for rule authoring. - -This is meant to be extensible for future validations. -Base rule validation and utility functions are defined here. -""" - -import logging -from typing import Any, Callable, Dict, List, Optional - -from pydantic import BaseModel - -from trestlebot import const - - -logger = logging.getLogger(__name__) - - -class RuleValidationError(BaseModel): - """RuleValidationError model.""" - - field_name: str - error_message: str - - -class ValidationOutcome(BaseModel): - """ValidationOutcome model.""" - - errors: List[RuleValidationError] - valid: bool - - -class ValidationHandler: - def __init__( # type: ignore - self, validate_fn: Callable[[Any, ValidationOutcome], None], next_handler=None - ) -> None: - self.validate_fn: Callable[[Any, ValidationOutcome], None] = validate_fn - self.next_handler: Optional[ValidationHandler] = next_handler - - def handle(self, data: Any, result: ValidationOutcome) -> None: - self.validate_fn(data, result) - if self.next_handler: - self.next_handler.handle(data, result) - - -def parameter_validation(data: Dict[str, Any], result: ValidationOutcome) -> None: - """Parameter logic additions validation.""" - rule_info: Dict[str, Any] = data.get(const.RULE_INFO_TAG, {}) - parameter_data: Dict[str, Any] = rule_info.get(const.PARAMETER, {}) - - if not parameter_data: - logger.debug("No parameter data found") - return # No parameter data, nothing to validate - - default_value = parameter_data.get(const.DEFAULT_VALUE, "") - alternative_values: Dict[str, Any] = parameter_data.get( - const.ALTERNATIVE_VALUES, {} - ) - - if not default_value: - add_validation_error(result, const.PARAMETER, "Default value is required") - - if not alternative_values: - add_validation_error(result, const.PARAMETER, "Alternative values are required") - - default_value_alt = alternative_values.get("default", "") - - if not default_value_alt or default_value_alt != default_value: - add_validation_error( - result, - const.PARAMETER, - "Default value must be one of the alternative values", - ) - - -def add_validation_error(result: ValidationOutcome, field: str, error_msg: str) -> None: - """Add a validation error to the result.""" - validation_error = RuleValidationError(field_name=field, error_message=error_msg) - result.errors.append(validation_error) - result.valid = False diff --git a/trestlebot/transformers/yaml_transformer.py b/trestlebot/transformers/yaml_transformer.py index 75d768d9..0fbcf81c 100644 --- a/trestlebot/transformers/yaml_transformer.py +++ b/trestlebot/transformers/yaml_transformer.py @@ -6,7 +6,7 @@ import logging import pathlib from io import StringIO -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from pydantic import ValidationError from ruamel.yaml import YAML @@ -23,8 +23,8 @@ Parameter, Profile, TrestleRule, + convert_errors, ) -from trestlebot.transformers.validations import ValidationHandler, ValidationOutcome logger = logging.getLogger(__name__) @@ -33,43 +33,52 @@ class ToRulesYAMLTransformer(ToRulesTransformer): """Interface for YAML transformer to Rules model.""" - def __init__(self, validator: Optional[ValidationHandler] = None) -> None: + def __init__(self) -> None: """Initialize.""" - self.validator: Optional[ValidationHandler] = validator super().__init__() def transform(self, blob: str) -> TrestleRule: """Transform YAML data into a TrestleRule object.""" + validation_errors: List[ValidationError] = [] try: yaml = YAML(typ="safe") yaml_data: Dict[str, Any] = yaml.load(blob) - logger.debug("Executing pre-validation on YAML data") - if self.validator is not None: - result = ValidationOutcome(errors=[], valid=True) - self.validator.handle(yaml_data, result) - if not result.valid: - raise RulesTransformerException( - f"Invalid YAML file: {result.errors}" - ) - rule_info_data = yaml_data[const.RULE_INFO_TAG] - profile_info_instance = Profile.parse_obj(rule_info_data[const.PROFILE]) + # Collecting validation errors for each field to + # get a comprehensive list of errors per YAML file. + try: + profile_info_instance = Profile.parse_obj(rule_info_data[const.PROFILE]) + except ValidationError as e: + validation_errors.append(e) - component_info_instance = ComponentInfo.parse_obj( - yaml_data[const.COMPONENT_INFO_TAG] - ) + try: + component_info_instance = ComponentInfo.parse_obj( + yaml_data[const.COMPONENT_INFO_TAG] + ) + except ValidationError as e: + validation_errors.append(e) parameter_instance: Optional[Parameter] = None if const.PARAMETER in rule_info_data: - parameter_instance = Parameter.parse_obj( - rule_info_data[const.PARAMETER] - ) + try: + parameter_instance = Parameter.parse_obj( + rule_info_data[const.PARAMETER] + ) + except ValidationError as e: + validation_errors.append(e) check_instance: Optional[Check] = None if const.CHECK in rule_info_data: - check_instance = Check.parse_obj(rule_info_data[const.CHECK]) + try: + check_instance = Check.parse_obj(rule_info_data[const.CHECK]) + except ValidationError as e: + validation_errors.append(e) + + if validation_errors: + pretty_errors = convert_errors(validation_errors) + raise RulesTransformerException(pretty_errors) rule_info_instance: TrestleRule = TrestleRule( name=rule_info_data[const.NAME], @@ -82,8 +91,6 @@ def transform(self, blob: str) -> TrestleRule: except KeyError as e: raise RulesTransformerException(f"Missing key in YAML file: {e}") - except ValidationError as e: - raise RulesTransformerException(f"Invalid YAML file: {e}") return rule_info_instance