diff --git a/.changelog/4739.yml b/.changelog/4739.yml new file mode 100644 index 00000000000..dde131fb0cb --- /dev/null +++ b/.changelog/4739.yml @@ -0,0 +1,5 @@ +changes: +- description: "This PR adds to the ST-110 validation which will now validate the sectionOrder and individual sections, + as well as adding a new validation, ST-111, which will validate the existence of the same fields." + type: feature +pr_number: 4739 diff --git a/demisto_sdk/commands/content_graph/strict_objects/base_strict_model.py b/demisto_sdk/commands/content_graph/strict_objects/base_strict_model.py index 37c8bd76bb7..f9984b03486 100644 --- a/demisto_sdk/commands/content_graph/strict_objects/base_strict_model.py +++ b/demisto_sdk/commands/content_graph/strict_objects/base_strict_model.py @@ -119,7 +119,10 @@ class StructureError(BaseStrictModel): def __str__(self): field_name = ",".join(more_itertools.always_iterable(self.field_name)) if self.error_type == "assertion_error": - error_message = f"The field {field_name} is not required, but should not be None if it exists" + error_message = ( + self.error_message + or f"An assertion error occurred for field {field_name}" + ) elif self.error_type == "value_error.extra": error_message = f"The field {field_name} is extra and {self.error_message}" elif self.error_type == "value_error.missing": diff --git a/demisto_sdk/commands/content_graph/strict_objects/common.py b/demisto_sdk/commands/content_graph/strict_objects/common.py index 221a33a33e5..4f9aab0c2a9 100644 --- a/demisto_sdk/commands/content_graph/strict_objects/common.py +++ b/demisto_sdk/commands/content_graph/strict_objects/common.py @@ -71,7 +71,9 @@ def prevent_none(cls, value, field): "breaking_changes_notes", # release-notes-config }: # The assertion is caught by pydantic and converted to a pydantic.ValidationError - assert value is not None, f"{value} may not be None" + assert ( + value is not None + ), f"The field {field.name} is not required, but should not be None if it exists" return value diff --git a/demisto_sdk/commands/content_graph/strict_objects/integration.py b/demisto_sdk/commands/content_graph/strict_objects/integration.py index b107773d229..55d8ebda94a 100644 --- a/demisto_sdk/commands/content_graph/strict_objects/integration.py +++ b/demisto_sdk/commands/content_graph/strict_objects/integration.py @@ -1,12 +1,13 @@ from typing import Any, List, Optional -from pydantic import Field +from pydantic import Field, conlist, validator from demisto_sdk.commands.common.constants import ( TYPE_PYTHON2, TYPE_PYTHON3, MarketplaceVersions, ) +from demisto_sdk.commands.common.StrEnum import StrEnum from demisto_sdk.commands.content_graph.strict_objects.base_strict_model import ( Argument, BaseIntegrationScript, @@ -137,13 +138,21 @@ class _CommonFieldsIntegration(BaseStrictModel): ) +class SectionOrderValues(StrEnum): + CONNECT = "Connect" + COLLECT = "Collect" + OPTIMIZE = "Optimize" + + class _StrictIntegration(BaseStrictModel): common_fields: CommonFieldsIntegration = Field(..., alias="commonfields") # type:ignore[valid-type] display: str beta: Optional[bool] = None category: str - section_order_pascal_case: Optional[List[str]] = Field(None, alias="sectionOrder") - section_order_lower_case: Optional[List[str]] = Field(None, alias="sectionorder") + section_order: Optional[conlist(SectionOrderValues, min_items=1, max_items=3)] = ( # type:ignore[valid-type] + Field(alias="sectionorder") + ) + configurations: List[Configuration] = Field(..., alias="configuration") # type:ignore[valid-type] image: Optional[str] = None description: str default_mapper_in: Optional[str] = Field(None, alias="defaultmapperin") @@ -152,7 +161,6 @@ class _StrictIntegration(BaseStrictModel): detailed_description: Optional[str] = Field(None, alias="detaileddescription") auto_config_instance: Optional[bool] = Field(None, alias="autoconfiginstance") support_level_header: MarketplaceVersions = Field(None, alias="supportlevelheader") - configuration: List[Configuration] # type:ignore[valid-type] script: Script # type:ignore[valid-type] hidden: Optional[bool] = None videos: Optional[List[str]] = None @@ -161,6 +169,41 @@ class _StrictIntegration(BaseStrictModel): script_not_visible: Optional[bool] = Field(None, alias="scriptNotVisible") hybrid: Optional[bool] = None + def __init__(self, **data): + """ + Initializes the _StrictIntegration object. + Using this custom init function to support two aliases for the section_order field. + """ + if "sectionOrder" in data and "sectionorder" not in data: + data["sectionorder"] = data.pop("sectionOrder") + elif "sectionOrder" in data and "sectionorder" in data: + data["sectionorder"] = list( + set(data["section_order"]) | set(data["section_order_camel_case"]) + ) + super().__init__(**data) + + @validator("configurations") + def validate_sections(cls, configurations, values): + """ + Validates each configuration object has a valid section clause. + A valid section clause is a section which is included in the list of the integration's section_order. + Even if the section is an allowed value (currently Collect, Connect or Optimize),it could be invalid if the + specific value is not present in section_order. + """ + section_order_field = values.get("section_order") + if not section_order_field: + return configurations + integration_sections = [ + section_name.value for section_name in section_order_field + ] + for config in configurations: + if not config.section: + return configurations + assert ( + config.section in integration_sections + ), f"section {config.section} of {config.display} is not present in section_order {integration_sections}" + return configurations + StrictIntegration = create_model( model_name="StrictIntegration", diff --git a/demisto_sdk/commands/content_graph/tests/test_data/integration.yml b/demisto_sdk/commands/content_graph/tests/test_data/integration.yml index 3abcbdd4371..dd0dda65f7e 100644 --- a/demisto_sdk/commands/content_graph/tests/test_data/integration.yml +++ b/demisto_sdk/commands/content_graph/tests/test_data/integration.yml @@ -7,17 +7,21 @@ fromversion: 5.0.0 category: Forensics & Malware Analysis description: Analyze files using the malwr sandbox detaileddescription: This integration uses the Marlwr sandbox API to submit, analyze and detonate files. +sectionorder: + - Connect configuration: - display: URL name: server defaultvalue: https://malwr.com type: 0 required: true + section: Connect - display: Credentials name: credentials defaultvalue: "" type: 9 required: true + section: Connect script: type: python subtype: python3 diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index 2ce92958150..3fb0e3b17bd 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -493,6 +493,7 @@ select = [ "MR107", "MR108", "ST110", + "ST111", "RN103", "RN105", "RN106", diff --git a/demisto_sdk/commands/validate/tests/ST_validators_test.py b/demisto_sdk/commands/validate/tests/ST_validators_test.py index 64651e36104..aa1e4e4ea69 100644 --- a/demisto_sdk/commands/validate/tests/ST_validators_test.py +++ b/demisto_sdk/commands/validate/tests/ST_validators_test.py @@ -15,6 +15,9 @@ from demisto_sdk.commands.validate.validators.ST_validators.ST110_is_valid_scheme import ( SchemaValidator, ) +from demisto_sdk.commands.validate.validators.ST_validators.ST111_no_exclusions_schema import ( + StrictSchemaValidator, +) from TestSuite.pack import Pack @@ -240,3 +243,168 @@ def test_pack_parser_errors_check(pack: Pack): "value is not a valid enumeration member; permitted: 'xsoar', 'partner', 'community', 'developer'", } == error_messages assert {"type_error.bool", "type_error.enum"} == error_types + + +def test_invalid_section_order(pack: Pack): + """ + Given: + - an integration which contains invalid section order + When: + - executing the IntegrationParser + Then: + - the integration is invalid and the correct error message is returned + """ + integration = pack.create_integration(yml=load_yaml("integration.yml")) + integration.yml.update({"sectionorder": ["Connect", "Run"]}) + + integration_parser = IntegrationParser( + Path(integration.path), list(MarketplaceVersions) + ) + + results = SchemaValidator().obtain_invalid_content_items([integration_parser]) + assert len(results) == 1 + assert results[0].message == ( + "Structure error (type_error.enum) in field sectionorder,1 of integration_0.yml: " + "value is not a valid enumeration member; permitted: " + "'Connect', 'Collect', 'Optimize'" + ) + + +def test_missing_section_order(pack: Pack): + """ + Given: + - an integration with a missing section order + When: + - executing the IntegrationParser + Then: + - the validation does not fail as it is only addressed in ST111 + """ + integration = pack.create_integration(yml=load_yaml("integration.yml")) + integration.yml.delete_key("sectionorder") + + integration_parser = IntegrationParser( + Path(integration.path), list(MarketplaceVersions) + ) + + results = SchemaValidator().obtain_invalid_content_items([integration_parser]) + assert len(results) == 0 + + +def test_invalid_section(pack: Pack): + """ + Given: + - an integration which contains invalid section clause in one of its configuration objects + When: + - executing the IntegrationParser + Then: + - the integration is invalid and the correct error message is returned + """ + integration = pack.create_integration(yml=load_yaml("integration.yml")) + curr_config = integration.yml.read_dict()["configuration"] + curr_config[0]["section"] = "Run" + integration.yml.update({"configuration": curr_config}) + + integration_parser = IntegrationParser( + Path(integration.path), list(MarketplaceVersions) + ) + + results = SchemaValidator().obtain_invalid_content_items([integration_parser]) + assert len(results) == 1 + assert results[0].message == ( + "Structure error (assertion_error) in field configuration of integration_0.yml: " + "section Run of URL is not present in section_order ['Connect']" + ) + + +def test_missing_section(pack: Pack): + """ + Given: + - an integration with a missing section clause in one of its configuration objects + When: + - executing the IntegrationParser + Then: + - the validation does not fail as it is only addressed in ST111 + """ + integration = pack.create_integration(yml=load_yaml("integration.yml")) + curr_config = integration.yml.read_dict()["configuration"] + curr_config[0].pop("section") + integration.yml.update({"configuration": curr_config}) + + integration_parser = IntegrationParser( + Path(integration.path), list(MarketplaceVersions) + ) + + results = SchemaValidator().obtain_invalid_content_items([integration_parser]) + assert len(results) == 0 + + +class TestST111: + def test_invalid_section_order(self): + """ + Given: + - an integration which contains invalid section order + When: + - executing the IntegrationParser + Then: + - the validation does not fail as it is only addressed in ST110 + """ + integration = create_integration_object( + paths=["sectionorder"], values=[["Connect", "Run"]] + ) + results = StrictSchemaValidator().obtain_invalid_content_items([integration]) + + assert len(results) == 0 + + def test_missing_section_order(self): + """ + Given: + - an integration with a missing section order + When: + - executing the IntegrationParser + Then: + - the integration is invalid and the correct error message is returned + """ + integration = create_integration_object() + integration.data.pop("sectionorder") + results = StrictSchemaValidator().obtain_invalid_content_items([integration]) + + assert len(results) == 1 + assert results[0].message == "Missing section order" + + def test_invalid_section(self): + """ + Given: + - an integration which contains invalid section clause in one of its configuration objects + When: + - executing the IntegrationParser + Then: + - the validation does not fail as it is only addressed in ST110 + """ + integration = create_integration_object() + curr_config = integration.data["configuration"] + curr_config[0]["section"] = "Run" + integration.data["configuration"] = curr_config + + results = StrictSchemaValidator().obtain_invalid_content_items([integration]) + assert len(results) == 0 + + def test_missing_section(self, pack: Pack): + """ + Given: + - an integration with a missing section clause in one of its configuration objects + When: + - executing the IntegrationParser + Then: + - the integration is invalid and the correct error message is returned + """ + integration = create_integration_object() + curr_config = integration.data["configuration"] + curr_config[0].pop("section") + integration.data["configuration"] = curr_config + + results = StrictSchemaValidator().obtain_invalid_content_items([integration]) + assert len(results) == 1 + assert ( + results[0].message + == f'Missing section for configuration {curr_config[0].get("name")}' + ) diff --git a/demisto_sdk/commands/validate/validators/ST_validators/ST110_is_valid_scheme.py b/demisto_sdk/commands/validate/validators/ST_validators/ST110_is_valid_scheme.py index d3a1e7cdc97..ff4d1ab8151 100644 --- a/demisto_sdk/commands/validate/validators/ST_validators/ST110_is_valid_scheme.py +++ b/demisto_sdk/commands/validate/validators/ST_validators/ST110_is_valid_scheme.py @@ -79,7 +79,9 @@ class SchemaValidator(BaseValidator[ContentTypes]): error_code = "ST110" - description = "Validate that the scheme's structure is valid." + description = ( + "Validate that the scheme's structure is valid, while excluding certain fields." + ) rationale = "Maintain valid structure for content items." def obtain_invalid_content_items( diff --git a/demisto_sdk/commands/validate/validators/ST_validators/ST111_no_exclusions_schema.py b/demisto_sdk/commands/validate/validators/ST_validators/ST111_no_exclusions_schema.py new file mode 100644 index 00000000000..f812df74be3 --- /dev/null +++ b/demisto_sdk/commands/validate/validators/ST_validators/ST111_no_exclusions_schema.py @@ -0,0 +1,50 @@ +from typing import Iterable + +from demisto_sdk.commands.content_graph.objects.integration import Integration +from demisto_sdk.commands.validate.validators.base_validator import ( + BaseValidator, + ValidationResult, +) + +ContentTypes = Integration + +ALLOWED_SECTIONS = [ + "Connect", + "Collect", + "Optimize", +] + + +class StrictSchemaValidator(BaseValidator[ContentTypes]): + error_code = "ST111" + description = "Validate that the scheme's structure is valid, no fields excluded." + rationale = "Maintain valid structure for content items." + + def obtain_invalid_content_items( + self, + content_items: Iterable[ContentTypes], + ) -> list[ValidationResult]: + invalid_content_items = [] + for content_item in content_items: + if error_message := self.is_missing_section_fields(content_item): + invalid_content_items.append( + ValidationResult( + validator=self, + message=error_message, + content_object=content_item, + ) + ) + return invalid_content_items + + def is_missing_section_fields(self, content_item: ContentTypes) -> str: + section_order = content_item.data.get("sectionorder") or content_item.data.get( + "sectionOrder" + ) + if not section_order: + return "Missing section order" + configurations = content_item.data.get("configuration") + for configuration in configurations: # type:ignore[union-attr] + section = configuration.get("section") + if not section: + return f'Missing section for configuration {configuration.get("name")}' + return ""