diff --git a/.changelog/5093.yml b/.changelog/5093.yml new file mode 100644 index 00000000000..948e29b1438 --- /dev/null +++ b/.changelog/5093.yml @@ -0,0 +1,4 @@ +changes: +- description: Added new AG109 and AG110 validations. + type: internal +pr_number: 5093 diff --git a/TestSuite/agentix_action.py b/TestSuite/agentix_action.py index 14b34d4462c..d88e32bf140 100644 --- a/TestSuite/agentix_action.py +++ b/TestSuite/agentix_action.py @@ -1,22 +1,27 @@ from pathlib import Path from typing import Optional +from demisto_sdk.commands.common.tools import set_value +from TestSuite.test_suite_base import TestSuiteBase from TestSuite.yml import YAML, yaml -class AgentixAction(YAML): +class AgentixAction(TestSuiteBase): def __init__(self, tmpdir: Path, name: str, repo): # Save entities + self.object_id = name self.name = name + self.node_id = name self._repo = repo self.repo_path = repo.path - self.path = str(tmpdir) - super().__init__(tmp_path=tmpdir / f"{self.name}.yml", repo_path=str(repo.path)) + self.path = tmpdir / f"{self.name}.yml" + self.yaml = YAML(tmp_path=self.path, repo_path=str(repo.path)) + super().__init__(self.path) @property def yml(self): # for backward compatible - return self + return self.yaml def build( self, @@ -24,7 +29,7 @@ def build( ): """Writes not None objects to files.""" if yml is not None: - self.write_dict(yml) + self.yaml.write_dict(yml) def create_default_agentix_action( self, @@ -44,3 +49,13 @@ def create_default_agentix_action( yml["id"] = action_id yml["name"] = name self.build(yml=yml) + + def set_agentix_action_name(self, name: str): + self.yml.update({"name": name}) + + def set_data(self, **key_path_to_val): + yml_contents = self.yml.read_dict() + for key_path, val in key_path_to_val.items(): + set_value(yml_contents, key_path, val) + self.yml.write_dict(yml_contents) + self.clear_from_path_cache() diff --git a/TestSuite/repo.py b/TestSuite/repo.py index 899435c63c7..64b4a52339c 100644 --- a/TestSuite/repo.py +++ b/TestSuite/repo.py @@ -88,6 +88,7 @@ def __init__(self, tmpdir: Path, init_git: bool = False): "GenericDefinitions": [], "Jobs": [], "Wizards": [], + "AgentixActions": [], } ) self.graph_interface: Optional[ContentGraphInterface] = None @@ -116,6 +117,12 @@ def setup_one_pack( pack = self.create_pack(name) pack.pack_metadata.update({"marketplaces": marketplaces}) + agentix_action = pack.create_agentix_action(f"{name}_agentix_action") + agentix_action.create_default_agentix_action() + agentix_action.yml.update({"commonfields": {"id": f"{name}_agentix_action"}}) + agentix_action.yml.update({"name": f"{name}_agentix_action"}) + agentix_action.yml.update({"display": f"{name}_agentix_action"}) + script = pack.create_script(f"{name}_script") script.create_default_script() script.yml.update({"commonfields": {"id": f"{name}_script"}}) diff --git a/demisto_sdk/commands/content_graph/interface/neo4j/neo4j_graph.py b/demisto_sdk/commands/content_graph/interface/neo4j/neo4j_graph.py index e67250feb97..99401152a94 100644 --- a/demisto_sdk/commands/content_graph/interface/neo4j/neo4j_graph.py +++ b/demisto_sdk/commands/content_graph/interface/neo4j/neo4j_graph.py @@ -73,6 +73,8 @@ validate_duplicate_ids, validate_fromversion, validate_marketplaces, + validate_multiple_agentix_actions_with_same_display_name, + validate_multiple_agentix_actions_with_same_name, validate_multiple_packs_with_same_display_name, validate_multiple_script_with_same_name, validate_packs_with_hidden_mandatory_dependencies, @@ -494,6 +496,24 @@ def validate_duplicate_ids( duplicate_models.append((self._id_to_obj[content_item.element_id], dups)) return duplicate_models + def validate_duplicate_agentix_action_display_names( + self, file_paths: List[str] + ) -> List[Tuple[str, List[str]]]: + with self.driver.session() as session: + results = session.execute_read( + validate_multiple_agentix_actions_with_same_display_name, file_paths + ) + return results + + def validate_duplicate_agentix_action_names( + self, file_paths: List[str] + ) -> List[Tuple[str, List[str]]]: + with self.driver.session() as session: + results = session.execute_read( + validate_multiple_agentix_actions_with_same_name, file_paths + ) + return results + def find_uses_paths_with_invalid_fromversion( self, file_paths: List[str], for_supported_versions=False ) -> List[BaseNode]: diff --git a/demisto_sdk/commands/content_graph/interface/neo4j/queries/validations.py b/demisto_sdk/commands/content_graph/interface/neo4j/queries/validations.py index 659c0b7e13f..18d8eae1dea 100644 --- a/demisto_sdk/commands/content_graph/interface/neo4j/queries/validations.py +++ b/demisto_sdk/commands/content_graph/interface/neo4j/queries/validations.py @@ -256,6 +256,44 @@ def validate_multiple_packs_with_same_display_name( ] +def validate_multiple_agentix_actions_with_same_display_name( + tx: Transaction, file_paths: List[str] +) -> List[Tuple[str, List[str]]]: + query = f"""// Returns all the Agentix Actions that have the same display name but different id +MATCH (a:{ContentType.AGENTIX_ACTION}), (b:{ContentType.AGENTIX_ACTION}) +WHERE a.display = b.display +""" + if file_paths: + query += f"AND a.path in {file_paths}" + query += """ +AND elementId(a) <> elementId(b) +RETURN a.object_id AS a_object_id, collect(b.object_id) AS b_object_ids +""" + return [ + (item.get("a_object_id"), item.get("b_object_ids")) + for item in run_query(tx, query) + ] + + +def validate_multiple_agentix_actions_with_same_name( + tx: Transaction, file_paths: List[str] +) -> List[Tuple[str, List[str]]]: + query = f"""// Returns all the Agentix Actions that have the same name but different id +MATCH (a:{ContentType.AGENTIX_ACTION}), (b:{ContentType.AGENTIX_ACTION}) +WHERE a.name = b.name +""" + if file_paths: + query += f"AND a.path in {file_paths}" + query += """ +AND elementId(a) <> elementId(b) +RETURN a.object_id AS a_object_id, collect(b.object_id) AS b_object_ids +""" + return [ + (item.get("a_object_id"), item.get("b_object_ids")) + for item in run_query(tx, query) + ] + + def validate_multiple_script_with_same_name( tx: Transaction, file_paths: List[str] ) -> Dict[str, str]: diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index 5c4a07935da..13a4406ea20 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -328,6 +328,8 @@ select = [ "AG101", "AG102", "AG103", + "AG109", + "AG110", "AG105", "AG106", "AG107", diff --git a/demisto_sdk/commands/validate/tests/AG_validators_test.py b/demisto_sdk/commands/validate/tests/AG_validators_test.py index 8ccae05dbcd..c701b3425d1 100644 --- a/demisto_sdk/commands/validate/tests/AG_validators_test.py +++ b/demisto_sdk/commands/validate/tests/AG_validators_test.py @@ -2,6 +2,7 @@ import pytest +from demisto_sdk.commands.common.hook_validations.base_validator import BaseValidator from demisto_sdk.commands.content_graph.objects.agentix_action import ( AgentixAction, ) @@ -10,6 +11,9 @@ from demisto_sdk.commands.validate.tests.test_tools import ( create_agentix_action_object, ) +from demisto_sdk.commands.validate.validators.AG_validators import ( + AG110_is_agentix_action_name_already_exists_valid, +) from demisto_sdk.commands.validate.validators.AG_validators.AG100_is_forbidden_content_item import ( IsForbiddenContentItemValidator, ) @@ -25,6 +29,10 @@ from demisto_sdk.commands.validate.validators.AG_validators.AG107_is_display_name_valid import ( IsDisplayNameValidValidator, ) +from demisto_sdk.commands.validate.validators.AG_validators.AG110_is_agentix_action_name_already_exists_valid import ( + IsAgentixActionNameAlreadyExistsValidator, +) +from TestSuite.repo import Repo def test_is_forbidden_content_item(): @@ -383,6 +391,41 @@ def test_is_type_valid(): ) in results[1].message +def test_IsAgentixActionNameAlreadyExistsValidator_obtain_invalid_content_items_using_graph( + mocker, graph_repo: Repo +): + """ + Given + - 3 packs, with 1 agentix action in each, and 2 of them are with the same name + When + - running IsAgentixActionNameAlreadyExistsValidator obtain_invalid_content_items function, on one of the packs with the duplicate agentix action name. + Then + - Validate that we got the error messages for the duplicate name. + """ + mocker.patch.object( + AG110_is_agentix_action_name_already_exists_valid, + "CONTENT_PATH", + new=graph_repo.path, + ) + graph_repo.setup_one_pack(name="pack1") + graph_repo.setup_one_pack(name="pack2") + graph_repo.setup_one_pack(name="pack3") + graph_repo.packs[1].agentix_actions[0].set_agentix_action_name("test") + graph_repo.packs[2].agentix_actions[0].set_agentix_action_name("test") + + BaseValidator.graph_interface = graph_repo.create_graph() + + results = IsAgentixActionNameAlreadyExistsValidator().obtain_invalid_content_items_using_graph( + [ + graph_repo.packs[0].agentix_actions[0], + graph_repo.packs[2].agentix_actions[0], + ], + validate_all_files=False, + ) + + assert len(results) == 1 + + @pytest.mark.parametrize( "content_items, expected_number_of_failures", [ diff --git a/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid.py b/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid.py new file mode 100644 index 00000000000..1ba5a515cb9 --- /dev/null +++ b/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from abc import ABC +from typing import Iterable, List + +from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH +from demisto_sdk.commands.content_graph.objects.agentix_action import AgentixAction +from demisto_sdk.commands.validate.validators.base_validator import ( + BaseValidator, + ValidationResult, +) + +ContentTypes = AgentixAction + + +class IsAgentixActionDisplayNameAlreadyExistsValidator( + BaseValidator[ContentTypes], ABC +): + error_code = "AG109" + description = "Validate that there are no duplicate display names of Agentix Actions in the repo." + rationale = "Prevent confusion between Agentix Actions." + error_message = "Agentix Action '{content_id}' has a duplicate display name as: {duplicate_display_name_ids}." + related_field = "display" + is_auto_fixable = False + + def obtain_invalid_content_items_using_graph( + self, content_items: Iterable[ContentTypes], validate_all_files: bool + ) -> List[ValidationResult]: + file_paths_to_objects = { + str(content_item.path.relative_to(CONTENT_PATH)): content_item + for content_item in content_items + } + content_id_to_objects = {item.object_id: item for item in content_items} # type: ignore[attr-defined] + + query_list = list(file_paths_to_objects) if not validate_all_files else [] + + query_results = self.graph.validate_duplicate_agentix_action_display_names( + query_list + ) + + return [ + ValidationResult( + validator=self, + message=self.error_message.format( + content_id=content_id, + duplicate_display_name_ids=(", ".join(duplicate_display_name_ids)), + ), + content_object=content_id_to_objects[content_id], + ) + for content_id, duplicate_display_name_ids in query_results + if content_id in content_id_to_objects + ] diff --git a/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid_all_files.py b/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid_all_files.py new file mode 100644 index 00000000000..d1c33d4cd90 --- /dev/null +++ b/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid_all_files.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import Iterable, List + +from demisto_sdk.commands.common.constants import ExecutionMode +from demisto_sdk.commands.content_graph.objects.integration import Integration +from demisto_sdk.commands.validate.validators.AG_validators.AG109_is_agentix_action_display_name_already_exists_valid import ( + IsAgentixActionDisplayNameAlreadyExistsValidator, +) +from demisto_sdk.commands.validate.validators.base_validator import ValidationResult + +ContentTypes = Integration + + +class IsAgentixActionDisplayNameAlreadyExistsValidatorAllFiles( + IsAgentixActionDisplayNameAlreadyExistsValidator +): + expected_execution_mode = [ExecutionMode.ALL_FILES] + + def obtain_invalid_content_items( + self, content_items: Iterable[ContentTypes] + ) -> List[ValidationResult]: + return self.obtain_invalid_content_items_using_graph(content_items, True) diff --git a/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid_list_files.py b/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid_list_files.py new file mode 100644 index 00000000000..253f0b3d78f --- /dev/null +++ b/demisto_sdk/commands/validate/validators/AG_validators/AG109_is_agentix_action_display_name_already_exists_valid_list_files.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import Iterable, List + +from demisto_sdk.commands.common.constants import ExecutionMode +from demisto_sdk.commands.content_graph.objects.integration import Integration +from demisto_sdk.commands.validate.validators.AG_validators.AG109_is_agentix_action_display_name_already_exists_valid import ( + IsAgentixActionDisplayNameAlreadyExistsValidator, +) +from demisto_sdk.commands.validate.validators.base_validator import ValidationResult + +ContentTypes = Integration + + +class IsAgentixActionDisplayNameAlreadyExistsValidatorListFiles( + IsAgentixActionDisplayNameAlreadyExistsValidator +): + expected_execution_mode = [ExecutionMode.SPECIFIC_FILES, ExecutionMode.USE_GIT] + + def obtain_invalid_content_items( + self, content_items: Iterable[ContentTypes] + ) -> List[ValidationResult]: + return self.obtain_invalid_content_items_using_graph(content_items, False) diff --git a/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid.py b/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid.py new file mode 100644 index 00000000000..d9b67b8d52d --- /dev/null +++ b/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from abc import ABC +from typing import Iterable, List + +from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH +from demisto_sdk.commands.content_graph.objects.agentix_action import AgentixAction +from demisto_sdk.commands.validate.validators.base_validator import ( + BaseValidator, + ValidationResult, +) + +ContentTypes = AgentixAction + + +class IsAgentixActionNameAlreadyExistsValidator(BaseValidator[ContentTypes], ABC): + error_code = "AG110" + description = ( + "Validate that there are no duplicate names of Agentix Actions in the repo." + ) + rationale = "Prevent confusion between Agentix Actions." + error_message = ( + "Agentix Action '{content_id}' has a duplicate name as: {duplicate_name_ids}." + ) + related_field = "name" + is_auto_fixable = False + + def obtain_invalid_content_items_using_graph( + self, content_items: Iterable[ContentTypes], validate_all_files: bool + ) -> List[ValidationResult]: + file_paths_to_objects = { + str(content_item.path.relative_to(CONTENT_PATH)): content_item + for content_item in content_items + } + content_id_to_objects = {item.object_id: item for item in content_items} # type: ignore[attr-defined] + + query_list = list(file_paths_to_objects) if not validate_all_files else [] + + query_results = self.graph.validate_duplicate_agentix_action_names(query_list) + + return [ + ValidationResult( + validator=self, + message=self.error_message.format( + content_id=content_id, + duplicate_name_ids=(", ".join(duplicate_name_ids)), + ), + content_object=content_id_to_objects[content_id], + ) + for content_id, duplicate_name_ids in query_results + if content_id in content_id_to_objects + ] diff --git a/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid_all_files.py b/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid_all_files.py new file mode 100644 index 00000000000..956e8efdb96 --- /dev/null +++ b/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid_all_files.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import Iterable, List + +from demisto_sdk.commands.common.constants import ExecutionMode +from demisto_sdk.commands.content_graph.objects.integration import Integration +from demisto_sdk.commands.validate.validators.AG_validators.AG110_is_agentix_action_name_already_exists_valid import ( + IsAgentixActionNameAlreadyExistsValidator, +) +from demisto_sdk.commands.validate.validators.base_validator import ValidationResult + +ContentTypes = Integration + + +class IsAgentixActionNameAlreadyExistsValidatorAllFiles( + IsAgentixActionNameAlreadyExistsValidator +): + expected_execution_mode = [ExecutionMode.ALL_FILES] + + def obtain_invalid_content_items( + self, content_items: Iterable[ContentTypes] + ) -> List[ValidationResult]: + return self.obtain_invalid_content_items_using_graph(content_items, True) diff --git a/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid_list_files.py b/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid_list_files.py new file mode 100644 index 00000000000..da7f5d2e38b --- /dev/null +++ b/demisto_sdk/commands/validate/validators/AG_validators/AG110_is_agentix_action_name_already_exists_valid_list_files.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import Iterable, List + +from demisto_sdk.commands.common.constants import ExecutionMode +from demisto_sdk.commands.content_graph.objects.integration import Integration +from demisto_sdk.commands.validate.validators.AG_validators.AG110_is_agentix_action_name_already_exists_valid import ( + IsAgentixActionNameAlreadyExistsValidator, +) +from demisto_sdk.commands.validate.validators.base_validator import ValidationResult + +ContentTypes = Integration + + +class IsAgentixActionNameAlreadyExistsValidatorListFiles( + IsAgentixActionNameAlreadyExistsValidator +): + expected_execution_mode = [ExecutionMode.SPECIFIC_FILES, ExecutionMode.USE_GIT] + + def obtain_invalid_content_items( + self, content_items: Iterable[ContentTypes] + ) -> List[ValidationResult]: + return self.obtain_invalid_content_items_using_graph(content_items, False)