From 683086fb50ba1065d4c2d307b16b66b97367da00 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Sun, 21 Dec 2025 15:09:38 +0200 Subject: [PATCH 01/20] added validation tests --- .../validate/sdk_validation_config.toml | 6 +- .../validate/tests/BA_validators_test.py | 215 ++++++++++++++++++ .../BA129_missing_compliant_policies.py | 111 +++++++++ 3 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index 5c4a07935d..b2440ca7e7 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -11,6 +11,7 @@ ignorable_errors = [ "BA124", "BA125", "BA127", + "BA129", "GF102", "DS108", "IF100", @@ -158,6 +159,7 @@ select = [ "BA119", "BA126", "BA128", + "BA129", "AG100", "AG101", "AG102", @@ -324,6 +326,7 @@ select = [ "BA126", "BA127", "BA128", + "BA129", "AG100", "AG101", "AG102", @@ -547,7 +550,8 @@ select = [ "LO107", "VC100", "VC101", - "BA112" + "BA112", + "BA129" ] warning = [ "GR109", diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index 5308ccc4a4..7485f2d900 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -118,6 +118,9 @@ from demisto_sdk.commands.validate.validators.BA_validators.BA128_is_command_or_script_name_starts_with_digit import ( IsCommandOrScriptNameStartsWithDigitValidator, ) +from demisto_sdk.commands.validate.validators.BA_validators.BA129_missing_compliant_policies import ( + MissingCompliantPoliciesValidator, +) from TestSuite.repo import ChangeCWD VALUE_WITH_TRAILING_SPACE = "field_with_space_should_fail " @@ -2948,3 +2951,215 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( assert len(result_messages) == expected_number_of_failures if expected_msgs: assert expected_msgs[0] in result_messages[0] +@pytest.mark.parametrize( + "content_items, expected_number_of_failures, expected_msgs", + [ + # Case 1: Valid Integration (IP Blockage) + ( + [ + create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "block-ip", + "arguments": [{"name": "ip_list"}], + "compliantpolicies": ["IP Blockage"], + } + ] + ], + ) + ], + 0, + [], + ), + # Case 2: Invalid Integration (Missing IP Blockage) + ( + [ + create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "block-ip", + "arguments": [{"name": "ip_list"}], + "compliantpolicies": [], + } + ] + ], + ) + ], + 1, + [ + "Command block-ip uses the arguments {'ip_list'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'IP Blockage'}." + ], + ), + # Case 3: Valid Script (User Soft Remediation) + ( + [ + create_script_object( + paths=["args", "compliantpolicies"], + values=[ + [{"name": "username"}], + ["User Soft Remediation"], + ], + ) + ], + 0, + [], + ), + # Case 4: Invalid Script (Missing EndPoint Isolation) + ( + [ + create_script_object( + name="IsolateEndpoint", + paths=["args", "compliantpolicies"], + values=[ + [{"name": "endpoint_id"}], + [], + ], + ) + ], + 1, + [ + "IsolateEndpoint uses the arguments {'endpoint_id'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'EndPoint Isolation'}." + ], + ), + # Case 5: Partial Failure (Multiple Policies) + # 'ip_list' is satisfied by 'IP Blockage', but 'username' is missing its policy. + ( + [ + create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "mixed-action", + "arguments": [{"name": "ip_list"}, {"name": "username"}], + "compliantpolicies": ["IP Blockage"], + } + ] + ], + ) + ], + 1, + [ + "Command mixed-action uses the arguments {'username'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'User Hard Remediation', 'User Soft Remediation'}." + ], + ), + # Case 6: No Policy Required + ( + [ + create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "simple-command", + "arguments": [{"name": "verbose"}], + "compliantpolicies": [], + } + ] + ], + ) + ], + 0, + [], + ), + # Case 7: Edge Case Valid - One of Two Policies Present + ( + [ + create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "hard-rem-command", + "arguments": [{"name": "username"}], + "compliantpolicies": ["User Hard Remediation"], + } + ] + ], + ) + ], + 0, + [], + ), + # Case 8: Edge Case Invalid - Neither Policy Present + ( + [ + create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "missing-both-command", + "arguments": [{"name": "username"}], + "compliantpolicies": [], + } + ] + ], + ) + ], + 1, + [ + "Command missing-both-command uses the arguments {'username'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'User Hard Remediation', 'User Soft Remediation'}." + ], + ), + ], +) +def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( + mocker, content_items, expected_number_of_failures, expected_msgs +): + """ + Tests BA129 with support for arguments that map to multiple policy options. + """ + mock_policies_content = { + "policies": [ + { + "name": "User Soft Remediation", + "category": "IAM", + "arguments": ["username", "user_id"], + }, + { + "name": "User Hard Remediation", + "category": "IAM", + "arguments": ["username"], + }, + { + "name": "IP Blockage", + "category": "EndPoint", + "arguments": ["ip_list", "ip_address"], + }, + { + "name": "EndPoint Isolation", + "category": "EndPoint", + "arguments": ["endpoint_id"], + }, + ] + } + + mocker.patch( + "demisto_sdk.commands.common.tools.is_external_repository", + return_value=False + ) + + mocker.patch( + "demisto_sdk.commands.common.tools.get_compliant_polices", + return_value=mock_policies_content["policies"], + ) + + validator = MissingCompliantPoliciesValidator() + results = validator.obtain_invalid_content_items(content_items) + + assert len(results) == expected_number_of_failures + + for result, expected_msg in zip(results, expected_msgs): + pass + + assert all( + [ + result.message == expected_msg + for result, expected_msg in zip(results, expected_msgs) + ] + ) \ No newline at end of file diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py new file mode 100644 index 0000000000..afc181689a --- /dev/null +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -0,0 +1,111 @@ + +from __future__ import annotations + +from typing import Iterable, List, Set, Union + +from demisto_sdk.commands.common.constants import GitStatuses,PARTNER_SUPPORT,COMMUNITY_SUPPORT +from demisto_sdk.commands.common.tools import get_compliant_polices +from demisto_sdk.commands.content_graph.objects.integration import Integration +from demisto_sdk.commands.content_graph.objects.script import Script +from demisto_sdk.commands.validate.validators.base_validator import ( + BaseValidator, + ValidationResult, +) + +ContentTypes = Union[Integration, Script] + + +class MissingCompliantPoliciesValidator(BaseValidator[ContentTypes]): + error_code = "BA129" + description = "Ensures that commands declare the appropriate compliantpolicies when using policy arguments." + rationale = "Certain command arguments are associated with compliance policies. This validation ensures that commands using such arguments explicitly declare the relevant policies in their YAML definition." + error_message = "{0} uses the arguments {1}, which are associated with one or more compliance policies, but does not declare the required compliantpolicies: {2}." + related_field = "compliantpolicies" + expected_git_statuses = [GitStatuses.ADDED, GitStatuses.MODIFIED] + is_auto_fixable = False + + def obtain_invalid_content_items( + self, + content_items: Iterable[ContentTypes], + ) -> List[ValidationResult]: + """ + Identify commands that use arguments associated with compliance policies + but do not declare the required compliantpolicies. + + Args: + content_items (Iterable[Integration | Script]): + Content graph items to validate. + validate_all_files (bool): + Indicates whether validation is running on all files or only on + files affected by the current git diff. + + Returns: + List[ValidationResult]: + Validation results for commands missing required compliantpolicies. + """ + results: list[ValidationResult] = [] + argument_to_policies = self._get_argument_to_policies_map() + + for content_item in content_items: + for command in self._get_commands(content_item): + argument_names: Set[str] = { + arg.name for arg in (command.args or []) if arg.name + } + declared_policies = set(command.compliantpolicies or []) + + problematic_arguments: Set[str] = set() + missing_policy_options: Set[str] = set() + + for arg in argument_names: + valid_policy_options = argument_to_policies.get(arg, set()) + + if not valid_policy_options: + continue + if valid_policy_options.isdisjoint(declared_policies): + problematic_arguments.add(arg) + missing_policy_options.update(valid_policy_options) + + if problematic_arguments: + results.append( + ValidationResult( + validator=self, + message=self.error_message.format( + f"Command {command.name}" if isinstance(content_item, Integration) else command.name, + problematic_arguments, + missing_policy_options, + ), + content_object=content_item, + related_field=f"commands.{command.name}.compliantpolicies", + ) + ) + + return results + + @staticmethod + def _get_commands(content_item: ContentTypes): + """ + Extract commands from an Integration or Script content item. + """ + if isinstance(content_item, Integration): + return content_item.commands or [] + elif isinstance(content_item, Script): + return [content_item] + return [] + + @staticmethod + def _get_argument_to_policies_map() -> dict[str, set[str]]: + """ + Build a mapping of argument names to compliance policy names + based on Config/compliant_policies.json. + """ + argument_to_policies: dict[str, set[str]] = {} + + for policy in get_compliant_polices(): + policy_name = policy.get("name") + if not policy_name: + continue + + for arg in policy.get("arguments", []): + argument_to_policies.setdefault(arg, set()).add(policy_name) + + return argument_to_policies From a2c3a2bc4263203fbfbbd683cbf527397b409140 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Sun, 21 Dec 2025 15:19:11 +0200 Subject: [PATCH 02/20] small sdk validation tool change --- demisto_sdk/commands/validate/sdk_validation_config.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index b2440ca7e7..8c8443d875 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -550,8 +550,7 @@ select = [ "LO107", "VC100", "VC101", - "BA112", - "BA129" + "BA112" ] warning = [ "GR109", From 8210b15b4021b4ddc3dd4c5a9b3da71432f290be Mon Sep 17 00:00:00 2001 From: almog2296 Date: Sun, 21 Dec 2025 15:22:16 +0200 Subject: [PATCH 03/20] add changelog --- .changelog/5168.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changelog/5168.yml diff --git a/.changelog/5168.yml b/.changelog/5168.yml new file mode 100644 index 0000000000..0d3837611f --- /dev/null +++ b/.changelog/5168.yml @@ -0,0 +1,4 @@ +changes: +- description: Adds a new ignorable validation (BA129) that ensures commands/scripts declare the appropriate compliantpolicies when using arguments associated with specific compliance standards (defined in compliant_policies.json). + type: feature +pr_number: 5168 From 350f0f91215d7d421c161cf2c784ac790012711b Mon Sep 17 00:00:00 2001 From: almog2296 Date: Mon, 22 Dec 2025 11:05:17 +0200 Subject: [PATCH 04/20] add tests --- .../validate/tests/BA_validators_test.py | 277 +++++++++++++----- .../BA129_missing_compliant_policies.py | 7 +- 2 files changed, 206 insertions(+), 78 deletions(-) diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index 7485f2d900..57382950c3 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -2951,10 +2951,12 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( assert len(result_messages) == expected_number_of_failures if expected_msgs: assert expected_msgs[0] in result_messages[0] + + @pytest.mark.parametrize( - "content_items, expected_number_of_failures, expected_msgs", + "content_items, expected_msgs", [ - # Case 1: Valid Integration (IP Blockage) + # I1: Valid Integration (IP Blockage) ( [ create_integration_object( @@ -2963,17 +2965,19 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( [ { "name": "block-ip", - "arguments": [{"name": "ip_list"}], + "description": "block ip", + "deprecated": False, + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], "compliantpolicies": ["IP Blockage"], } ] ], ) ], - 0, [], ), - # Case 2: Invalid Integration (Missing IP Blockage) + # I2: Invalid Integration (Missing IP Blockage) ( [ create_integration_object( @@ -2982,51 +2986,68 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( [ { "name": "block-ip", - "arguments": [{"name": "ip_list"}], + "description": "block ip", + "deprecated": False, + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], "compliantpolicies": [], } ] ], ) ], - 1, [ - "Command block-ip uses the arguments {'ip_list'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'IP Blockage'}." + "Command block-ip uses the arguments: ['ip_list'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['IP Blockage']." ], ), - # Case 3: Valid Script (User Soft Remediation) + # I3: Partial Failure (Multiple Policies) - one arg satisfied, one not ( [ - create_script_object( - paths=["args", "compliantpolicies"], + create_integration_object( + paths=["script.commands"], values=[ - [{"name": "username"}], - ["User Soft Remediation"], + [ + { + "name": "mixed-action", + "description": "mixed action", + "deprecated": False, + "arguments": [ + {"name": "ip_list", "description": "ip list"}, + {"name": "username", "description": "username"}, + ], + "outputs": [], + "compliantpolicies": ["IP Blockage"], + } + ] ], ) ], - 0, - [], + [ + "Command mixed-action uses the arguments: ['username'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['User Hard Remediation', 'User Soft Remediation']." + ], ), - # Case 4: Invalid Script (Missing EndPoint Isolation) + # I4: No Policy Required (arg not in mapping) ( [ - create_script_object( - name="IsolateEndpoint", - paths=["args", "compliantpolicies"], + create_integration_object( + paths=["script.commands"], values=[ - [{"name": "endpoint_id"}], - [], + [ + { + "name": "simple-command", + "description": "simple command", + "deprecated": False, + "arguments": [{"name": "verbose", "description": "verbose"}], + "outputs": [], + "compliantpolicies": [], + } + ] ], ) ], - 1, - [ - "IsolateEndpoint uses the arguments {'endpoint_id'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'EndPoint Isolation'}." - ], + [], ), - # Case 5: Partial Failure (Multiple Policies) - # 'ip_list' is satisfied by 'IP Blockage', but 'username' is missing its policy. + # I5: Edge Valid - One of two policies present (username) ( [ create_integration_object( @@ -3034,20 +3055,20 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( values=[ [ { - "name": "mixed-action", - "arguments": [{"name": "ip_list"}, {"name": "username"}], - "compliantpolicies": ["IP Blockage"], + "name": "hard-rem-command", + "description": "hard remediation", + "deprecated": False, + "arguments": [{"name": "username", "description": "username"}], + "outputs": [], + "compliantpolicies": ["User Hard Remediation"], } ] ], ) ], - 1, - [ - "Command mixed-action uses the arguments {'username'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'User Hard Remediation', 'User Soft Remediation'}." - ], + [], ), - # Case 6: No Policy Required + # I6: Multi-command integration: one passes, one fails ( [ create_integration_object( @@ -3055,18 +3076,30 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( values=[ [ { - "name": "simple-command", - "arguments": [{"name": "verbose"}], + "name": "cmd-ok", + "description": "ok", + "deprecated": False, + "arguments": [{"name": "ip_list", "description": "ip"}], + "outputs": [], + "compliantpolicies": ["IP Blockage"], + }, + { + "name": "cmd-bad", + "description": "bad", + "deprecated": False, + "arguments": [{"name": "endpoint_id", "description": "id"}], + "outputs": [], "compliantpolicies": [], - } + }, ] ], ) ], - 0, - [], + [ + "Command cmd-bad uses the arguments: ['endpoint_id'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['EndPoint Isolation']." + ], ), - # Case 7: Edge Case Valid - One of Two Policies Present + # I7: Multiple problematic args in a single command (union policies + both args) ( [ create_integration_object( @@ -3074,18 +3107,25 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( values=[ [ { - "name": "hard-rem-command", - "arguments": [{"name": "username"}], - "compliantpolicies": ["User Hard Remediation"], + "name": "double-bad", + "description": "double bad", + "deprecated": False, + "arguments": [ + {"name": "ip_list", "description": "ip"}, + {"name": "endpoint_id", "description": "id"}, + ], + "outputs": [], + "compliantpolicies": [], } ] ], ) ], - 0, - [], + [ + "Command double-bad uses the arguments: ['endpoint_id', 'ip_list'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['EndPoint Isolation', 'IP Blockage']." + ], ), - # Case 8: Edge Case Invalid - Neither Policy Present + # I8: Multiple problematic commands ( [ create_integration_object( @@ -3093,28 +3133,130 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( values=[ [ { - "name": "missing-both-command", - "arguments": [{"name": "username"}], + "name": "cmd-bad-1", + "description": "bad 1", + "deprecated": False, + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], "compliantpolicies": [], - } + }, + { + "name": "cmd-bad-2", + "description": "bad 2", + "deprecated": False, + "arguments": [{"name": "endpoint_id", "description": "endpoint id"}], + "outputs": [], + "compliantpolicies": [], + }, ] ], ) ], - 1, [ - "Command missing-both-command uses the arguments {'username'}, which are associated with one or more compliance policies, but does not declare any of the required options. Missing policies (at least one required per argument): {'User Hard Remediation', 'User Soft Remediation'}." + "Command cmd-bad-1 uses the arguments: ['ip_list'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['IP Blockage'].", + "Command cmd-bad-2 uses the arguments: ['endpoint_id'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['EndPoint Isolation'].", + ], + ), + # S1: Valid Script (User Soft Remediation) + ( + [ + create_script_object( + paths=["args", "compliantpolicies"], + values=[ + [{"name": "username", "description": "username"}], + ["User Soft Remediation"], + ], + ) + ], + [], + ), + # S2: Invalid Script (Missing EndPoint Isolation) + ( + [ + create_script_object( + paths=["args", "compliantpolicies"], + values=[ + [{"name": "endpoint_id", "description": "endpoint id"}], + [], + ], + ) + ], + [ + "myScript uses the arguments: ['endpoint_id'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['EndPoint Isolation']." + ], + ), + # S3: Script multi-policy valid (one-of set) + ( + [ + create_script_object( + paths=["args", "compliantpolicies"], + values=[ + [{"name": "username", "description": "username"}], + ["User Hard Remediation"], + ], + ) + ], + [], + ), + # S4: Script multi-policy invalid (none present) + ( + [ + create_script_object( + paths=["args", "compliantpolicies"], + values=[ + [{"name": "username", "description": "username"}], + [], + ], + ) + ], + [ + "myScript uses the arguments: ['username'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['User Hard Remediation', 'User Soft Remediation']." ], ), + # S5: Script no-policy-required arg only + ( + [ + create_script_object( + paths=["args", "compliantpolicies"], + values=[ + [{"name": "verbose", "description": "verbose"}], + [], + ], + ) + ], + [], + ), ], ) def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( - mocker, content_items, expected_number_of_failures, expected_msgs + mocker, content_items, expected_msgs ): """ - Tests BA129 with support for arguments that map to multiple policy options. - """ - mock_policies_content = { + Given + - A list of content items (Integrations and Scripts) that may define commands/arguments which are associated with compliance policies. + When + - Calling the MissingCompliantPoliciesValidator.obtain_invalid_content_items function. + Then + - Make sure the returned ValidationResult messages match the expected failures for each scenario. + + Test cases (Integrations): + - Valid integration command where the required compliantpolicies are declared for a policy-associated argument. + - Invalid integration command missing required compliantpolicies for a policy-associated argument. + - Integration command with multiple arguments where one argument is satisfied by declared policies and another is missing required policies. + - Integration command using an argument that is not associated with any compliance policy (no failure expected). + - Integration command where the argument maps to multiple valid policies and declaring one of them is sufficient (no failure expected). + - Integration with multiple commands where one command is valid and another command is missing required compliantpolicies (single failure expected). + - Integration command with multiple policy-associated arguments where none of the required policies are declared (union of missing policies and args in the failure). + - Integration with multiple commands where multiple commands are missing required compliantpolicies (multiple failures expected). + + Test cases (Scripts): + - Valid script where the required compliantpolicies are declared for a policy-associated argument. + - Invalid script missing required compliantpolicies for a policy-associated argument. + - Script where the argument maps to multiple valid policies and declaring one of them is sufficient (no failure expected). + - Script where the argument maps to multiple valid policies and none are declared (failure expected). + - Script using an argument that is not associated with any compliance policy (no failure expected). + """ + mock_policies_dict = { "policies": [ { "name": "User Soft Remediation", @@ -3124,7 +3266,7 @@ def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( { "name": "User Hard Remediation", "category": "IAM", - "arguments": ["username"], + "arguments": ["username"], }, { "name": "IP Blockage", @@ -3139,27 +3281,14 @@ def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( ] } - mocker.patch( - "demisto_sdk.commands.common.tools.is_external_repository", - return_value=False - ) + mocker.patch("demisto_sdk.commands.common.tools.is_external_repository", return_value=False) mocker.patch( - "demisto_sdk.commands.common.tools.get_compliant_polices", - return_value=mock_policies_content["policies"], + "demisto_sdk.commands.common.tools.get_dict_from_file", + return_value=(mock_policies_dict, "Config/compliant_policies.json"), ) validator = MissingCompliantPoliciesValidator() results = validator.obtain_invalid_content_items(content_items) - assert len(results) == expected_number_of_failures - - for result, expected_msg in zip(results, expected_msgs): - pass - - assert all( - [ - result.message == expected_msg - for result, expected_msg in zip(results, expected_msgs) - ] - ) \ No newline at end of file + assert [r.message for r in results] == expected_msgs diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index afc181689a..aed740e007 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -19,9 +19,8 @@ class MissingCompliantPoliciesValidator(BaseValidator[ContentTypes]): error_code = "BA129" description = "Ensures that commands declare the appropriate compliantpolicies when using policy arguments." rationale = "Certain command arguments are associated with compliance policies. This validation ensures that commands using such arguments explicitly declare the relevant policies in their YAML definition." - error_message = "{0} uses the arguments {1}, which are associated with one or more compliance policies, but does not declare the required compliantpolicies: {2}." + error_message = "{0} uses the arguments: {1}, which are associated with one or more compliance policies, but does not declare the required compliantpolicies: {2}." related_field = "compliantpolicies" - expected_git_statuses = [GitStatuses.ADDED, GitStatuses.MODIFIED] is_auto_fixable = False def obtain_invalid_content_items( @@ -71,8 +70,8 @@ def obtain_invalid_content_items( validator=self, message=self.error_message.format( f"Command {command.name}" if isinstance(content_item, Integration) else command.name, - problematic_arguments, - missing_policy_options, + sorted(problematic_arguments), + sorted(missing_policy_options), ), content_object=content_item, related_field=f"commands.{command.name}.compliantpolicies", From bd1a3710d22535e9f7f4f51fd93d82fcdb5562dc Mon Sep 17 00:00:00 2001 From: almog2296 Date: Tue, 23 Dec 2025 10:30:08 +0200 Subject: [PATCH 05/20] fix ruff --- .../validate/tests/BA_validators_test.py | 36 ++++++++++++++----- .../BA129_missing_compliant_policies.py | 9 ++--- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index 57382950c3..bafc46d19d 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -2967,7 +2967,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "block-ip", "description": "block ip", "deprecated": False, - "arguments": [{"name": "ip_list", "description": "ip list"}], + "arguments": [ + {"name": "ip_list", "description": "ip list"} + ], "outputs": [], "compliantpolicies": ["IP Blockage"], } @@ -2988,7 +2990,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "block-ip", "description": "block ip", "deprecated": False, - "arguments": [{"name": "ip_list", "description": "ip list"}], + "arguments": [ + {"name": "ip_list", "description": "ip list"} + ], "outputs": [], "compliantpolicies": [], } @@ -3037,7 +3041,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "simple-command", "description": "simple command", "deprecated": False, - "arguments": [{"name": "verbose", "description": "verbose"}], + "arguments": [ + {"name": "verbose", "description": "verbose"} + ], "outputs": [], "compliantpolicies": [], } @@ -3058,7 +3064,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "hard-rem-command", "description": "hard remediation", "deprecated": False, - "arguments": [{"name": "username", "description": "username"}], + "arguments": [ + {"name": "username", "description": "username"} + ], "outputs": [], "compliantpolicies": ["User Hard Remediation"], } @@ -3079,7 +3087,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "cmd-ok", "description": "ok", "deprecated": False, - "arguments": [{"name": "ip_list", "description": "ip"}], + "arguments": [ + {"name": "ip_list", "description": "ip"} + ], "outputs": [], "compliantpolicies": ["IP Blockage"], }, @@ -3087,7 +3097,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "cmd-bad", "description": "bad", "deprecated": False, - "arguments": [{"name": "endpoint_id", "description": "id"}], + "arguments": [ + {"name": "endpoint_id", "description": "id"} + ], "outputs": [], "compliantpolicies": [], }, @@ -3136,7 +3148,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "cmd-bad-1", "description": "bad 1", "deprecated": False, - "arguments": [{"name": "ip_list", "description": "ip list"}], + "arguments": [ + {"name": "ip_list", "description": "ip list"} + ], "outputs": [], "compliantpolicies": [], }, @@ -3144,7 +3158,9 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "cmd-bad-2", "description": "bad 2", "deprecated": False, - "arguments": [{"name": "endpoint_id", "description": "endpoint id"}], + "arguments": [ + {"name": "endpoint_id", "description": "endpoint id"} + ], "outputs": [], "compliantpolicies": [], }, @@ -3281,7 +3297,9 @@ def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( ] } - mocker.patch("demisto_sdk.commands.common.tools.is_external_repository", return_value=False) + mocker.patch( + "demisto_sdk.commands.common.tools.is_external_repository", return_value=False + ) mocker.patch( "demisto_sdk.commands.common.tools.get_dict_from_file", diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index aed740e007..b8e471e44b 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -3,13 +3,12 @@ from typing import Iterable, List, Set, Union -from demisto_sdk.commands.common.constants import GitStatuses,PARTNER_SUPPORT,COMMUNITY_SUPPORT from demisto_sdk.commands.common.tools import get_compliant_polices from demisto_sdk.commands.content_graph.objects.integration import Integration from demisto_sdk.commands.content_graph.objects.script import Script from demisto_sdk.commands.validate.validators.base_validator import ( - BaseValidator, - ValidationResult, + BaseValidator, + ValidationResult, ) ContentTypes = Union[Integration, Script] @@ -69,7 +68,9 @@ def obtain_invalid_content_items( ValidationResult( validator=self, message=self.error_message.format( - f"Command {command.name}" if isinstance(content_item, Integration) else command.name, + f"Command {command.name}" + if isinstance(content_item, Integration) + else command.name, sorted(problematic_arguments), sorted(missing_policy_options), ), From 256e73c2f53ac85f4973b361fda86425bc292c66 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Tue, 23 Dec 2025 10:56:01 +0200 Subject: [PATCH 06/20] fix ruff --- .../commands/validate/tests/BA_validators_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index bafc46d19d..da0efc074a 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -3087,9 +3087,7 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "name": "cmd-ok", "description": "ok", "deprecated": False, - "arguments": [ - {"name": "ip_list", "description": "ip"} - ], + "arguments": [{"name": "ip_list", "description": "ip"}], "outputs": [], "compliantpolicies": ["IP Blockage"], }, @@ -3159,7 +3157,10 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "description": "bad 2", "deprecated": False, "arguments": [ - {"name": "endpoint_id", "description": "endpoint id"} + { + "name": "endpoint_id", + "description": "endpoint id" + } ], "outputs": [], "compliantpolicies": [], From 4c3127585585dc0b2cb309685453ecaa5f8ba423 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Tue, 23 Dec 2025 11:29:19 +0200 Subject: [PATCH 07/20] fix ruff --- demisto_sdk/commands/validate/tests/BA_validators_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index da0efc074a..38c302cd49 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -3159,7 +3159,7 @@ def test_MarketplaceTagsValidator_obtain_invalid_content_items( "arguments": [ { "name": "endpoint_id", - "description": "endpoint id" + "description": "endpoint id", } ], "outputs": [], From 5feb2ded12fd1379423b5a35c6a7adbc9c820b96 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Tue, 23 Dec 2025 12:48:53 +0200 Subject: [PATCH 08/20] fix ruff --- .../validators/BA_validators/BA129_missing_compliant_policies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index b8e471e44b..bfce3b8158 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -1,4 +1,3 @@ - from __future__ import annotations from typing import Iterable, List, Set, Union From faada8d4b1230e560a9c8b7faa52cfa279b7314d Mon Sep 17 00:00:00 2001 From: almog2296 Date: Tue, 23 Dec 2025 13:48:49 +0200 Subject: [PATCH 09/20] check only changed/new commands --- .../validate/tests/BA_validators_test.py | 69 +++++++++++++++++++ .../BA129_missing_compliant_policies.py | 63 +++++++++++++---- 2 files changed, 119 insertions(+), 13 deletions(-) diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index 38c302cd49..f000282347 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -3311,3 +3311,72 @@ def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( results = validator.obtain_invalid_content_items(content_items) assert [r.message for r in results] == expected_msgs + + +def test_MissingCompliantPoliciesValidator_unchanged_command_is_ignored(mocker): + """ + Given: + - An Integration content item with a command 'block-ip' that uses argument 'ip_list'. + - The command is technically INVALID (it is missing the 'IP Blockage' policy). + - However, an 'old_base_content_object' exists with the EXACT same command structure. + + When: + - Calling obtain_invalid_content_items. + + Then: + - The validator should detect that the command has not changed compared to the old version. + - It should return an empty list (ignoring the missing policy error for legacy/unchanged commands). + """ + mock_policies_dict = { + "policies": [ + { + "name": "IP Blockage", + "category": "EndPoint", + "arguments": ["ip_list"], + }, + ] + } + mocker.patch( + "demisto_sdk.commands.common.tools.is_external_repository", return_value=False + ) + mocker.patch( + "demisto_sdk.commands.common.tools.get_dict_from_file", + return_value=(mock_policies_dict, "Config/compliant_policies.json"), + ) + + current_integration = create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "block-ip", + "description": "block ip", + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], + "compliantpolicies": [], + } + ] + ], + ) + + old_integration = create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "block-ip", + "description": "block ip", + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], + "compliantpolicies": [], + } + ] + ], + ) + + current_integration.old_base_content_object = old_integration + + validator = MissingCompliantPoliciesValidator() + results = validator.obtain_invalid_content_items([current_integration]) + + assert results == [] diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index bfce3b8158..fa275c4b64 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable, List, Set, Union +from typing import Iterable, List, Set, Union, Optional from demisto_sdk.commands.common.tools import get_compliant_polices from demisto_sdk.commands.content_graph.objects.integration import Integration @@ -28,23 +28,22 @@ def obtain_invalid_content_items( """ Identify commands that use arguments associated with compliance policies but do not declare the required compliantpolicies. - - Args: - content_items (Iterable[Integration | Script]): - Content graph items to validate. - validate_all_files (bool): - Indicates whether validation is running on all files or only on - files affected by the current git diff. - - Returns: - List[ValidationResult]: - Validation results for commands missing required compliantpolicies. + Only validates new or modified commands. """ results: list[ValidationResult] = [] argument_to_policies = self._get_argument_to_policies_map() for content_item in content_items: for command in self._get_commands(content_item): + + # Check if command is new or modified + old_command = self._get_old_command(content_item, command.name) + + # If the command existed before and hasn't changed relevant fields, skip validation + if old_command and not self._has_command_changed(old_command, command): + continue + + # Validation Logic argument_names: Set[str] = { arg.name for arg in (command.args or []) if arg.name } @@ -58,6 +57,7 @@ def obtain_invalid_content_items( if not valid_policy_options: continue + # Check if the declared policies cover the requirements for this arg if valid_policy_options.isdisjoint(declared_policies): problematic_arguments.add(arg) missing_policy_options.update(valid_policy_options) @@ -80,6 +80,43 @@ def obtain_invalid_content_items( return results + def _get_old_command(self, content_item: ContentTypes, command_name: str) -> Optional[object]: + """ + Retrieves the corresponding command object from the old content item. + """ + old_content_item = content_item.old_base_content_object + if not old_content_item: + return None + + if isinstance(old_content_item, Script): + # For Scripts, the content item itself acts as the command + return old_content_item if old_content_item.name == command_name else None + # For Integrations, search through the list of commands + if isinstance(old_content_item, Integration): + for command in old_content_item.commands: + if command.name == command_name: + return command + return None + + @staticmethod + def _has_command_changed(old_command, new_command) -> bool: + """ + Checks if relevant fields (arguments or compliantpolicies) have changed between versions. + """ + # Compare Arguments (by name) + old_args = {arg.name for arg in (old_command.args or [])} + new_args = {arg.name for arg in (new_command.args or [])} + if old_args != new_args: + return True + + # Compare Compliant Policies + old_policies = set(old_command.compliantpolicies or []) + new_policies = set(new_command.compliantpolicies or []) + if old_policies != new_policies: + return True + + return False + @staticmethod def _get_commands(content_item: ContentTypes): """ @@ -107,4 +144,4 @@ def _get_argument_to_policies_map() -> dict[str, set[str]]: for arg in policy.get("arguments", []): argument_to_policies.setdefault(arg, set()).add(policy_name) - return argument_to_policies + return argument_to_policies \ No newline at end of file From 6115a664106f0272876680cbe7bb49dab5212e83 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Wed, 24 Dec 2025 10:47:28 +0200 Subject: [PATCH 10/20] small update --- .../validate/sdk_validation_config.toml | 1 - .../BA129_missing_compliant_policies.py | 127 +++++++++--------- 2 files changed, 60 insertions(+), 68 deletions(-) diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index 8c8443d875..97500fe8ad 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -11,7 +11,6 @@ ignorable_errors = [ "BA124", "BA125", "BA127", - "BA129", "GF102", "DS108", "IF100", diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index fa275c4b64..ec20d84b75 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -2,6 +2,7 @@ from typing import Iterable, List, Set, Union, Optional +from demisto_sdk.commands.common.constants import GitStatuses from demisto_sdk.commands.common.tools import get_compliant_polices from demisto_sdk.commands.content_graph.objects.integration import Integration from demisto_sdk.commands.content_graph.objects.script import Script @@ -9,7 +10,6 @@ BaseValidator, ValidationResult, ) - ContentTypes = Union[Integration, Script] @@ -34,94 +34,94 @@ def obtain_invalid_content_items( argument_to_policies = self._get_argument_to_policies_map() for content_item in content_items: - for command in self._get_commands(content_item): - - # Check if command is new or modified - old_command = self._get_old_command(content_item, command.name) - - # If the command existed before and hasn't changed relevant fields, skip validation - if old_command and not self._has_command_changed(old_command, command): - continue - - # Validation Logic - argument_names: Set[str] = { - arg.name for arg in (command.args or []) if arg.name - } - declared_policies = set(command.compliantpolicies or []) - - problematic_arguments: Set[str] = set() - missing_policy_options: Set[str] = set() - - for arg in argument_names: - valid_policy_options = argument_to_policies.get(arg, set()) - - if not valid_policy_options: - continue - # Check if the declared policies cover the requirements for this arg - if valid_policy_options.isdisjoint(declared_policies): - problematic_arguments.add(arg) - missing_policy_options.update(valid_policy_options) - - if problematic_arguments: - results.append( - ValidationResult( - validator=self, - message=self.error_message.format( - f"Command {command.name}" - if isinstance(content_item, Integration) - else command.name, - sorted(problematic_arguments), - sorted(missing_policy_options), - ), - content_object=content_item, - related_field=f"commands.{command.name}.compliantpolicies", - ) - ) + commands_to_validate = [] + if content_item.git_status == GitStatuses.ADDED: + commands_to_validate = self._get_commands(content_item) + else: + for command in self._get_commands(content_item): + old_command = self._get_old_command(content_item, command.name) + if not old_command or self._has_command_arguments_changed(old_command, command): + commands_to_validate.append(command) + + for command in commands_to_validate: + validation_result = self._check_command_compliance( + command, content_item, argument_to_policies + ) + if validation_result: + results.append(validation_result) return results + def _check_command_compliance( + self, + command, + content_item: ContentTypes, + argument_to_policies: dict[str, set[str]] + ) -> Optional[ValidationResult]: + """ + Helper method to validate a single command against the policy map. + """ + argument_names: Set[str] = {arg.name for arg in (command.args or []) if arg.name} + declared_policies = set(command.compliantpolicies or []) + + problematic_arguments: Set[str] = set() + missing_policy_options: Set[str] = set() + + for arg in argument_names: + valid_policy_options = argument_to_policies.get(arg, set()) + + if not valid_policy_options: + continue + # Check if the declared policies cover the requirements for this arg + if valid_policy_options.isdisjoint(declared_policies): + problematic_arguments.add(arg) + missing_policy_options.update(valid_policy_options) + + if problematic_arguments: + return ValidationResult( + validator=self, + message=self.error_message.format( + f"Command {command.name}" if isinstance(content_item, Integration) else command.name, + sorted(problematic_arguments), + sorted(missing_policy_options), + ), + content_object=content_item, + related_field=f"commands.{command.name}.compliantpolicies", + ) + return None + def _get_old_command(self, content_item: ContentTypes, command_name: str) -> Optional[object]: """ Retrieves the corresponding command object from the old content item. """ old_content_item = content_item.old_base_content_object if not old_content_item: - return None + return None # Changed from False to None for correct typing if isinstance(old_content_item, Script): - # For Scripts, the content item itself acts as the command - return old_content_item if old_content_item.name == command_name else None - # For Integrations, search through the list of commands + return old_content_item + if isinstance(old_content_item, Integration): for command in old_content_item.commands: if command.name == command_name: return command + return None @staticmethod - def _has_command_changed(old_command, new_command) -> bool: + def _has_command_arguments_changed(old_command, new_command) -> bool: """ - Checks if relevant fields (arguments or compliantpolicies) have changed between versions. + Checks if arguments have changed. """ - # Compare Arguments (by name) old_args = {arg.name for arg in (old_command.args or [])} new_args = {arg.name for arg in (new_command.args or [])} if old_args != new_args: return True - # Compare Compliant Policies - old_policies = set(old_command.compliantpolicies or []) - new_policies = set(new_command.compliantpolicies or []) - if old_policies != new_policies: - return True - return False @staticmethod def _get_commands(content_item: ContentTypes): - """ - Extract commands from an Integration or Script content item. - """ if isinstance(content_item, Integration): return content_item.commands or [] elif isinstance(content_item, Script): @@ -130,18 +130,11 @@ def _get_commands(content_item: ContentTypes): @staticmethod def _get_argument_to_policies_map() -> dict[str, set[str]]: - """ - Build a mapping of argument names to compliance policy names - based on Config/compliant_policies.json. - """ argument_to_policies: dict[str, set[str]] = {} - for policy in get_compliant_polices(): policy_name = policy.get("name") if not policy_name: continue - for arg in policy.get("arguments", []): argument_to_policies.setdefault(arg, set()).add(policy_name) - return argument_to_policies \ No newline at end of file From 574269379cec4407b529942910e623feadb41344 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Wed, 24 Dec 2025 14:30:06 +0200 Subject: [PATCH 11/20] ruff fix --- .../BA129_missing_compliant_policies.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index ec20d84b75..6045fe2e35 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -10,6 +10,7 @@ BaseValidator, ValidationResult, ) + ContentTypes = Union[Integration, Script] @@ -40,7 +41,9 @@ def obtain_invalid_content_items( else: for command in self._get_commands(content_item): old_command = self._get_old_command(content_item, command.name) - if not old_command or self._has_command_arguments_changed(old_command, command): + if not old_command or self._has_command_arguments_changed( + old_command, command + ): commands_to_validate.append(command) for command in commands_to_validate: @@ -56,12 +59,14 @@ def _check_command_compliance( self, command, content_item: ContentTypes, - argument_to_policies: dict[str, set[str]] + argument_to_policies: dict[str, set[str]], ) -> Optional[ValidationResult]: """ Helper method to validate a single command against the policy map. """ - argument_names: Set[str] = {arg.name for arg in (command.args or []) if arg.name} + argument_names: Set[str] = { + arg.name for arg in (command.args or []) if arg.name + } declared_policies = set(command.compliantpolicies or []) problematic_arguments: Set[str] = set() @@ -81,7 +86,9 @@ def _check_command_compliance( return ValidationResult( validator=self, message=self.error_message.format( - f"Command {command.name}" if isinstance(content_item, Integration) else command.name, + f"Command {command.name}" + if isinstance(content_item, Integration) + else command.name, sorted(problematic_arguments), sorted(missing_policy_options), ), @@ -90,7 +97,9 @@ def _check_command_compliance( ) return None - def _get_old_command(self, content_item: ContentTypes, command_name: str) -> Optional[object]: + def _get_old_command( + self, content_item: ContentTypes, command_name: str + ) -> Optional[object]: """ Retrieves the corresponding command object from the old content item. """ @@ -137,4 +146,4 @@ def _get_argument_to_policies_map() -> dict[str, set[str]]: continue for arg in policy.get("arguments", []): argument_to_policies.setdefault(arg, set()).add(policy_name) - return argument_to_policies \ No newline at end of file + return argument_to_policies From 96a711c891ad46491222fcd57957d16714724559 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Wed, 24 Dec 2025 15:04:32 +0200 Subject: [PATCH 12/20] ruff fix --- .../BA_validators/BA129_missing_compliant_policies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index 6045fe2e35..5e1dc41a64 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable, List, Set, Union, Optional +from typing import Iterable, List, Optional, Set, Union from demisto_sdk.commands.common.constants import GitStatuses from demisto_sdk.commands.common.tools import get_compliant_polices From c42a00b7f365f8eba4b2ebbe644fae28200aeefc Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 25 Dec 2025 08:41:11 +0200 Subject: [PATCH 13/20] update changelogs --- .changelog/5168.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/5168.yml b/.changelog/5168.yml index 0d3837611f..ce78124423 100644 --- a/.changelog/5168.yml +++ b/.changelog/5168.yml @@ -1,4 +1,4 @@ changes: -- description: Adds a new ignorable validation (BA129) that ensures commands/scripts declare the appropriate compliantpolicies when using arguments associated with specific compliance standards (defined in compliant_policies.json). +- description: Adds a new validation (BA129) that ensures commands/scripts declare the appropriate compliantpolicies when using arguments associated with specific compliance standards (defined in compliant_policies.json). type: feature pr_number: 5168 From 542011bfeef9af92fb63477b525c0d85fee580b8 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 25 Dec 2025 09:29:52 +0200 Subject: [PATCH 14/20] add more docstrings --- .../BA129_missing_compliant_policies.py | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index 5e1dc41a64..1577ff84da 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -29,7 +29,13 @@ def obtain_invalid_content_items( """ Identify commands that use arguments associated with compliance policies but do not declare the required compliantpolicies. - Only validates new or modified commands. + Only identify newly commands, or commands with arguments change. + + Args: + content_items (Iterable[ContentTypes]): A list of Integration or Script objects to validate. + + Returns: + List[ValidationResult]: A list of validation results for any commands failing the policy check. """ results: list[ValidationResult] = [] argument_to_policies = self._get_argument_to_policies_map() @@ -63,6 +69,13 @@ def _check_command_compliance( ) -> Optional[ValidationResult]: """ Helper method to validate a single command against the policy map. + Args: + command: The command object to validate. + content_item (ContentTypes): The parent Integration or Script object. + argument_to_policies (dict[str, set[str]]): A map with argument names to their required policies. + + Returns: + Optional[ValidationResult]: A ValidationResult if the command is non-compliant, otherwise None. """ argument_names: Set[str] = { arg.name for arg in (command.args or []) if arg.name @@ -102,10 +115,16 @@ def _get_old_command( ) -> Optional[object]: """ Retrieves the corresponding command object from the old content item. + Args: + content_item (ContentTypes): The current content item. + command_name (str): The name of the command to look up. + + Returns: + Optional[object]: The old command object if found, otherwise None. """ old_content_item = content_item.old_base_content_object if not old_content_item: - return None # Changed from False to None for correct typing + return None if isinstance(old_content_item, Script): return old_content_item @@ -121,6 +140,12 @@ def _get_old_command( def _has_command_arguments_changed(old_command, new_command) -> bool: """ Checks if arguments have changed. + Args: + old_command: The command object from the previous version. + new_command: The current command object. + + Returns: + bool: True if the set of argument names differs, False otherwise. """ old_args = {arg.name for arg in (old_command.args or [])} new_args = {arg.name for arg in (new_command.args or [])} @@ -130,7 +155,19 @@ def _has_command_arguments_changed(old_command, new_command) -> bool: return False @staticmethod - def _get_commands(content_item: ContentTypes): + def _get_commands(content_item: ContentTypes) -> List[object]: + """ + Extracts the list of command objects from the content item. + + For Integrations, this returns the list of defined commands. + For Scripts, which act as a single command, this returns a list containing the script object itself. + + Args: + content_item (ContentTypes): The Integration or Script object. + + Returns: + List[object]: A list of command objects to be validated. + """ if isinstance(content_item, Integration): return content_item.commands or [] elif isinstance(content_item, Script): @@ -139,6 +176,16 @@ def _get_commands(content_item: ContentTypes): @staticmethod def _get_argument_to_policies_map() -> dict[str, set[str]]: + """ + Builds a lookup mapping of argument names to their associated compliance policies. + + It iterates through the compliant policies configuration (retrieved via `get_compliant_polices`) + and maps every argument listed in a policy to the policy's name. This allows for easy + lookup to check if an argument requires specific compliance policies. + + Returns: + dict[str, set[str]]: A dictionary where keys are argument names and values are sets of policy names associated with that argument. + """ argument_to_policies: dict[str, set[str]] = {} for policy in get_compliant_polices(): policy_name = policy.get("name") From f9ddbb42367db0df30662c7c2a5dc81d0bbe0f18 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 25 Dec 2025 10:28:44 +0200 Subject: [PATCH 15/20] fix ruff --- .../BA_validators/BA129_missing_compliant_policies.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index 1577ff84da..b45ba970e7 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable, List, Optional, Set, Union +from typing import Any, Iterable, List, Optional, Set, Union from demisto_sdk.commands.common.constants import GitStatuses from demisto_sdk.commands.common.tools import get_compliant_polices @@ -112,7 +112,7 @@ def _check_command_compliance( def _get_old_command( self, content_item: ContentTypes, command_name: str - ) -> Optional[object]: + ) -> Optional[Any]: """ Retrieves the corresponding command object from the old content item. Args: @@ -120,7 +120,7 @@ def _get_old_command( command_name (str): The name of the command to look up. Returns: - Optional[object]: The old command object if found, otherwise None. + Optional[Any]: The old command object if found, otherwise None. """ old_content_item = content_item.old_base_content_object if not old_content_item: @@ -155,7 +155,7 @@ def _has_command_arguments_changed(old_command, new_command) -> bool: return False @staticmethod - def _get_commands(content_item: ContentTypes) -> List[object]: + def _get_commands(content_item: ContentTypes) -> List[Any]: """ Extracts the list of command objects from the content item. @@ -166,7 +166,7 @@ def _get_commands(content_item: ContentTypes) -> List[object]: content_item (ContentTypes): The Integration or Script object. Returns: - List[object]: A list of command objects to be validated. + List[Any]: A list of command objects to be validated. """ if isinstance(content_item, Integration): return content_item.commands or [] From cd3b05658bb769805559086cd020ea09d0af062b Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 25 Dec 2025 11:19:21 +0200 Subject: [PATCH 16/20] sdk validation config change --- demisto_sdk/commands/validate/sdk_validation_config.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index 97500fe8ad..9e8b9552ab 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -158,7 +158,6 @@ select = [ "BA119", "BA126", "BA128", - "BA129", "AG100", "AG101", "AG102", From c596e3a19737398baa8010f82880bdaf79a48f20 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 25 Dec 2025 15:43:06 +0200 Subject: [PATCH 17/20] added ignorbale + only a new commands --- .../validate/sdk_validation_config.toml | 1 + .../validate/tests/BA_validators_test.py | 93 ++++++++++++++++++- .../BA129_missing_compliant_policies.py | 39 ++------ 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/demisto_sdk/commands/validate/sdk_validation_config.toml b/demisto_sdk/commands/validate/sdk_validation_config.toml index 9e8b9552ab..94e1441ad3 100644 --- a/demisto_sdk/commands/validate/sdk_validation_config.toml +++ b/demisto_sdk/commands/validate/sdk_validation_config.toml @@ -11,6 +11,7 @@ ignorable_errors = [ "BA124", "BA125", "BA127", + "BA129", "GF102", "DS108", "IF100", diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index f000282347..d0d3775cec 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -3313,12 +3313,12 @@ def test_MissingCompliantPoliciesValidator_obtain_invalid_content_items( assert [r.message for r in results] == expected_msgs -def test_MissingCompliantPoliciesValidator_unchanged_command_is_ignored(mocker): +def test_MissingCompliantPoliciesValidator_old_command_is_ignored(mocker): """ Given: - An Integration content item with a command 'block-ip' that uses argument 'ip_list'. - The command is technically INVALID (it is missing the 'IP Blockage' policy). - - However, an 'old_base_content_object' exists with the EXACT same command structure. + - However, an 'old_base_content_object' exists with the EXACT same command name. When: - Calling obtain_invalid_content_items. @@ -3366,7 +3366,7 @@ def test_MissingCompliantPoliciesValidator_unchanged_command_is_ignored(mocker): { "name": "block-ip", "description": "block ip", - "arguments": [{"name": "ip_list", "description": "ip list"}], + "arguments": [{"name": "ip", "description": "ip list"}], "outputs": [], "compliantpolicies": [], } @@ -3380,3 +3380,90 @@ def test_MissingCompliantPoliciesValidator_unchanged_command_is_ignored(mocker): results = validator.obtain_invalid_content_items([current_integration]) assert results == [] + + +def test_MissingCompliantPoliciesValidator_only_new_command_is_reported(mocker): + """ + Given: + - An Integration content item with TWO commands: + 1) 'old-cmd' (exists in old_base_content_object) - INVALID but should be ignored. + 2) 'new-cmd' (does NOT exist in old_base_content_object) - INVALID and should be reported. + - Both commands use args that require compliantpolicies, and both are missing them. + + When: + - Calling obtain_invalid_content_items. + + Then: + - Only the NEW command should produce a ValidationResult. + """ + mock_policies_dict = { + "policies": [ + { + "name": "IP Blockage", + "category": "EndPoint", + "arguments": ["ip_list"], + }, + { + "name": "EndPoint Isolation", + "category": "EndPoint", + "arguments": ["endpoint_id"], + }, + ] + } + + mocker.patch( + "demisto_sdk.commands.common.tools.is_external_repository", return_value=False + ) + mocker.patch( + "demisto_sdk.commands.common.tools.get_dict_from_file", + return_value=(mock_policies_dict, "Config/compliant_policies.json"), + ) + + current_integration = create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "old-cmd", + "description": "old cmd", + "deprecated": False, + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], + "compliantpolicies": [], # INVALID, but should be ignored (old command) + }, + { + "name": "new-cmd", + "description": "new cmd", + "deprecated": False, + "arguments": [{"name": "endpoint_id", "description": "endpoint id"}], + "outputs": [], + "compliantpolicies": [], # INVALID, and should be reported (new command) + }, + ] + ], + ) + + old_integration = create_integration_object( + paths=["script.commands"], + values=[ + [ + { + "name": "old-cmd", + "description": "old cmd", + "deprecated": False, + "arguments": [{"name": "ip_list", "description": "ip list"}], + "outputs": [], + "compliantpolicies": [], # Still invalid, but existence is what matters here + } + ] + ], + ) + + current_integration.old_base_content_object = old_integration + + validator = MissingCompliantPoliciesValidator() + results = validator.obtain_invalid_content_items([current_integration]) + + assert [r.message for r in results] == [ + "Command new-cmd uses the arguments: ['endpoint_id'], which are associated with one or more compliance policies, but does not declare the required compliantpolicies: ['EndPoint Isolation']." + ] diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index b45ba970e7..3138bdea4a 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -46,10 +46,7 @@ def obtain_invalid_content_items( commands_to_validate = self._get_commands(content_item) else: for command in self._get_commands(content_item): - old_command = self._get_old_command(content_item, command.name) - if not old_command or self._has_command_arguments_changed( - old_command, command - ): + if self._is_new_command(content_item, command.name): commands_to_validate.append(command) for command in commands_to_validate: @@ -110,49 +107,31 @@ def _check_command_compliance( ) return None - def _get_old_command( + def _is_new_command( self, content_item: ContentTypes, command_name: str - ) -> Optional[Any]: + ) -> bool: """ - Retrieves the corresponding command object from the old content item. + Check if the given command is new command. Args: content_item (ContentTypes): The current content item. command_name (str): The name of the command to look up. Returns: - Optional[Any]: The old command object if found, otherwise None. + bool: wether the command is new. """ old_content_item = content_item.old_base_content_object if not old_content_item: - return None + return True if isinstance(old_content_item, Script): - return old_content_item + return old_content_item.name != command_name if isinstance(old_content_item, Integration): for command in old_content_item.commands: if command.name == command_name: - return command - - return None - - @staticmethod - def _has_command_arguments_changed(old_command, new_command) -> bool: - """ - Checks if arguments have changed. - Args: - old_command: The command object from the previous version. - new_command: The current command object. - - Returns: - bool: True if the set of argument names differs, False otherwise. - """ - old_args = {arg.name for arg in (old_command.args or [])} - new_args = {arg.name for arg in (new_command.args or [])} - if old_args != new_args: - return True + return False - return False + return True @staticmethod def _get_commands(content_item: ContentTypes) -> List[Any]: From 99648b96e6e269dea2e9999b30d28ad02dc8653a Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 25 Dec 2025 15:53:57 +0200 Subject: [PATCH 18/20] ruff fix --- demisto_sdk/commands/validate/tests/BA_validators_test.py | 4 +++- .../BA_validators/BA129_missing_compliant_policies.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demisto_sdk/commands/validate/tests/BA_validators_test.py b/demisto_sdk/commands/validate/tests/BA_validators_test.py index d0d3775cec..560db06695 100644 --- a/demisto_sdk/commands/validate/tests/BA_validators_test.py +++ b/demisto_sdk/commands/validate/tests/BA_validators_test.py @@ -3435,7 +3435,9 @@ def test_MissingCompliantPoliciesValidator_only_new_command_is_reported(mocker): "name": "new-cmd", "description": "new cmd", "deprecated": False, - "arguments": [{"name": "endpoint_id", "description": "endpoint id"}], + "arguments": [ + {"name": "endpoint_id", "description": "endpoint id"} + ], "outputs": [], "compliantpolicies": [], # INVALID, and should be reported (new command) }, diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index 3138bdea4a..8149793428 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -107,9 +107,7 @@ def _check_command_compliance( ) return None - def _is_new_command( - self, content_item: ContentTypes, command_name: str - ) -> bool: + def _is_new_command(self, content_item: ContentTypes, command_name: str) -> bool: """ Check if the given command is new command. Args: From dec338a226879dfcfde4f2e1938261207dea1df5 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 8 Jan 2026 13:42:01 +0200 Subject: [PATCH 19/20] fix after review --- .../BA_validators/BA129_missing_compliant_policies.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index 8149793428..e5da457e55 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -29,7 +29,7 @@ def obtain_invalid_content_items( """ Identify commands that use arguments associated with compliance policies but do not declare the required compliantpolicies. - Only identify newly commands, or commands with arguments change. + Only identify newly commands. Args: content_items (Iterable[ContentTypes]): A list of Integration or Script objects to validate. @@ -87,10 +87,11 @@ def _check_command_compliance( if not valid_policy_options: continue - # Check if the declared policies cover the requirements for this arg - if valid_policy_options.isdisjoint(declared_policies): + # Check if and which required policies for this argument are missing + missing_for_arg = valid_policy_options - declared_policies + if missing_for_arg: problematic_arguments.add(arg) - missing_policy_options.update(valid_policy_options) + missing_policy_options.update(missing_for_arg) if problematic_arguments: return ValidationResult( From 3bc5b8d846d20489e5088596cdc0e7601320e524 Mon Sep 17 00:00:00 2001 From: almog2296 Date: Thu, 8 Jan 2026 16:51:06 +0200 Subject: [PATCH 20/20] fix after review --- .../BA_validators/BA129_missing_compliant_policies.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py index e5da457e55..0cb2d543f3 100644 --- a/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py +++ b/demisto_sdk/commands/validate/validators/BA_validators/BA129_missing_compliant_policies.py @@ -87,11 +87,10 @@ def _check_command_compliance( if not valid_policy_options: continue - # Check if and which required policies for this argument are missing - missing_for_arg = valid_policy_options - declared_policies - if missing_for_arg: + # Check if the declared policies cover the requirements for this arg + if valid_policy_options.isdisjoint(declared_policies): problematic_arguments.add(arg) - missing_policy_options.update(missing_for_arg) + missing_policy_options.update(valid_policy_options) if problematic_arguments: return ValidationResult(