Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b079531
extend
michal-dagan Oct 15, 2025
145af58
extend
michal-dagan Oct 15, 2025
3d62d03
extend
michal-dagan Oct 15, 2025
c985122
Merge remote-tracking branch 'origin/master' into GR110-extend-inputs…
michal-dagan Oct 16, 2025
a6da9e5
extend
michal-dagan Oct 16, 2025
b5fb691
extend
michal-dagan Oct 16, 2025
ce8ab81
extend
michal-dagan Oct 17, 2025
f99bc5f
extend
michal-dagan Oct 18, 2025
820a9cd
Merge branch 'master' into GR110-extend-inputs-outputs
michal-dagan Oct 18, 2025
ee32bcd
extend
michal-dagan Oct 19, 2025
3b75516
Merge remote-tracking branch 'origin/GR110-extend-inputs-outputs' int…
michal-dagan Oct 19, 2025
d21d47e
added playbooks
michal-dagan Oct 20, 2025
2b52de9
Merge branch 'master' into GR110-extend-inputs-outputs
michal-dagan Oct 20, 2025
add3490
fix
michal-dagan Nov 17, 2025
7308ea3
fix
michal-dagan Nov 17, 2025
a3c1639
fix
michal-dagan Nov 17, 2025
3eafe58
fix
michal-dagan Nov 17, 2025
1743050
Merge branch 'master' into extend-gr-110
michal-dagan Nov 17, 2025
befea77
fix
michal-dagan Nov 17, 2025
885b7bd
fix
michal-dagan Nov 17, 2025
231c28f
Merge branch 'master' into extend-gr-110
michal-dagan Nov 17, 2025
097c7a5
Merge branch 'master' into extend-gr-110
michal-dagan Nov 18, 2025
d38043e
CR fix
michal-dagan Nov 23, 2025
bae5017
Merge branch 'master' into extend-gr-110
michal-dagan Nov 23, 2025
51ce7e4
CR fix
michal-dagan Nov 24, 2025
6345733
Merge remote-tracking branch 'origin/extend-gr-110' into extend-gr-110
michal-dagan Nov 24, 2025
5a3ea3f
CR fix
michal-dagan Nov 24, 2025
534db66
Merge branch 'master' into extend-gr-110
michal-dagan Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/5134.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changes:
- description: Enhanced GR110 validation to ensure Agentix Actions reference valid underlying content items (commands, scripts, and playbooks) with matching inputs and outputs.
type: feature
pr_number: 5134
6 changes: 6 additions & 0 deletions demisto_sdk/commands/content_graph/interface/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ def export_graph(
def get_unknown_content_uses(self, file_paths: List[str]) -> List[BaseNode]:
pass

@abstractmethod
def get_agentix_actions_using_content_items(
self, content_item_ids: List[str]
) -> List[BaseNode]:
pass

@abstractmethod
def get_duplicate_pack_display_name(
self, file_paths: List[str]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
get_targets_by_path,
)
from demisto_sdk.commands.content_graph.interface.neo4j.queries.validations import (
get_agentix_actions_using_content_items,
get_items_using_deprecated,
get_supported_modules_mismatch_commands,
get_supported_modules_mismatch_content_items,
Expand Down Expand Up @@ -449,6 +450,17 @@ def get_unknown_playbook_tests(
self._add_relationships_to_objects(session, results)
return [self._id_to_obj[result] for result in results]

def get_agentix_actions_using_content_items(
self, content_item_ids: List[str]
) -> List[BaseNode]:
with self.driver.session() as session:
agentix_action_nodes = session.execute_read(
get_agentix_actions_using_content_items,
content_item_ids,
)
self._add_nodes_to_mapping(agentix_action_nodes)
return [self._id_to_obj[node.element_id] for node in agentix_action_nodes]

def get_duplicate_pack_display_name(
self, file_paths: List[str]
) -> List[Tuple[str, List[str]]]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,56 @@ def get_supported_modules_mismatch_content_items(
)
results[item.get("content_item").element_id] = neo_res
return results


def get_agentix_actions_using_content_items(
tx: Transaction, content_item_ids: List[str]
) -> List[graph.Node]:
"""
Query graph to return all AgentixActions that use the specified
Integration, Script, or Playbook IDs.

This function finds AgentixActions that depend on the given Integrations,
Scripts, or Playbooks, either directly (for Scripts/Playbooks) or through commands
(for Integrations).

When an Integration/Script/Playbook is modified, we need to validate ALL
AgentixActions that depend on it, regardless of pack, since a breaking
change affects all dependents.

Args:
tx: The Transaction to contact the graph with.
content_item_ids: List of Integration, Script, or Playbook object IDs to find
dependent AgentixActions for. If empty, returns ALL AgentixActions.

Returns:
List of AgentixAction nodes that use the specified content items.
"""

# Build filter clause - only filter by IDs if list is provided
id_filter = (
f"content_item.object_id IN {content_item_ids} AND " if content_item_ids else ""
)

query = f"""
// Find AgentixActions using commands from specified Integrations
MATCH (agentix_action:{ContentType.AGENTIX_ACTION})-[:{RelationshipType.USES}]->(c:{ContentType.COMMAND})<-[:{RelationshipType.HAS_COMMAND}]-(content_item:{ContentType.INTEGRATION})
WHERE {id_filter}{is_target_available("agentix_action", "content_item")}
RETURN agentix_action

UNION

// Find AgentixActions using Scripts directly
MATCH (agentix_action:{ContentType.AGENTIX_ACTION})-[:{RelationshipType.USES}]->(content_item:{ContentType.SCRIPT})
WHERE {id_filter}{is_target_available("agentix_action", "content_item")}
RETURN agentix_action

UNION

// Find AgentixActions using Playbooks directly
MATCH (agentix_action:{ContentType.AGENTIX_ACTION})-[:{RelationshipType.USES}]->(content_item:{ContentType.PLAYBOOK})
WHERE {id_filter}{is_target_available("agentix_action", "content_item")}
RETURN agentix_action
"""
items = run_query(tx, query)
return [item.get("agentix_action") for item in items]
21 changes: 21 additions & 0 deletions demisto_sdk/commands/content_graph/parsers/agentix_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(
self.requires_user_approval: Optional[bool] = self.yml_data.get(
"requiresuserapproval"
)
self.connect_to_dependencies()

@cached_property
def field_mapping(self):
Expand All @@ -60,3 +61,23 @@ def outputs(self) -> list[Any]:
@property
def few_shots(self) -> Optional[list[str]]:
return self.yml_data.get("fewshots", [])

def connect_to_dependencies(self) -> None:
"""Create USES relationship to the underlying content item."""
# Determine the target content type based on underlying item type
if self.underlying_content_item_type == "command":
# For commands, use USES_COMMAND_OR_SCRIPT with the command name
if self.underlying_content_item_command:
self.add_command_or_script_dependency(
self.underlying_content_item_command, is_mandatory=True
)
elif self.underlying_content_item_type == "script":
# For scripts, use USES_BY_ID with the script ID
self.add_dependency_by_id(
self.underlying_content_item_id, ContentType.SCRIPT, is_mandatory=True
)
elif self.underlying_content_item_type == "playbook":
# For playbooks, use USES_BY_ID with the playbook ID
self.add_dependency_by_id(
self.underlying_content_item_id, ContentType.PLAYBOOK, is_mandatory=True
)
233 changes: 233 additions & 0 deletions demisto_sdk/commands/validate/tests/GR_validators_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

from demisto_sdk.commands.common.constants import MarketplaceVersions
from demisto_sdk.commands.content_graph.objects.conf_json import ConfJSON
from demisto_sdk.commands.validate.tests.test_tools import (
create_agentix_action_object,
)
from demisto_sdk.commands.validate.validators.base_validator import BaseValidator
from demisto_sdk.commands.validate.validators.GR_validators import (
GR104_is_pack_display_name_already_exists,
Expand Down Expand Up @@ -67,6 +70,9 @@
from demisto_sdk.commands.validate.validators.GR_validators.GR109_is_supported_modules_compatibility_list_files import (
IsSupportedModulesCompatibilityListFiles,
)
from demisto_sdk.commands.validate.validators.GR_validators.GR110_is_agentix_action_using_existing_content_item_valid import (
IsAgentixActionUsingExistingContentItemValidator,
)
from TestSuite.repo import Repo

MP_XSOAR = [MarketplaceVersions.XSOAR.value]
Expand Down Expand Up @@ -1757,3 +1763,230 @@ def test_SupportedModulesCompatibility_invalid_list_files_mismatch_playbook(
== "Module compatibility issue detected: Content item 'playbook1' has incompatible commands: [command_x]. Make sure the commands used are supported by the same modules as the content item."
)
assert results[0].content_object.object_id == "playbook1"


@pytest.fixture
def repo_for_test_gr_110(graph_repo: Repo):
"""
Creates a test repository for testing GR110 validator.
"""
# Pack 1: Simple integration and script
pack_1 = graph_repo.create_pack("Pack1")

# Create integration with commands
integration = pack_1.create_integration("MyIntegration")
integration.set_commands(["test-command", "get-incidents"])

# Create simple script
pack_1.create_script("MyScript")

# Create simple playbook
pack_1.create_playbook("MyPlaybook")

return graph_repo


def test_gr110_missing_underlying_command(repo_for_test_gr_110: Repo):
"""
Given:
- An Agentix Action referencing a non-existing command.
When:
- Running the GR110 validator.
Then:
- A validation error should be returned indicating the content item was not found.
"""
action = create_agentix_action_object(
action_name="TestAction",
paths=[
"underlyingcontentitem.name",
"underlyingcontentitem.id",
"underlyingcontentitem.type",
"underlyingcontentitem.command",
],
values=[
"MyIntegration",
"MyIntegration",
"command",
"nonexistent-command",
],
)

graph_interface = repo_for_test_gr_110.create_graph()
BaseValidator.graph_interface = graph_interface

validator = IsAgentixActionUsingExistingContentItemValidator()
results = validator.obtain_invalid_content_items_using_graph([action], False)

assert "could not be found in the Content repository" in results[0].message


def test_gr110_unsupported_content_type(repo_for_test_gr_110: Repo):
"""
Given:
- An Agentix Action referencing an unsupported content type (widget).
When:
- Running the GR110 validator.
Then:
- A validation error should be returned about unsupported content type.
"""
action = create_agentix_action_object(
action_name="TestAction",
paths=[
"underlyingcontentitem.name",
"underlyingcontentitem.id",
"underlyingcontentitem.type",
],
values=[
"MyIntegration",
"MyIntegration",
"widget", # unsupported type
],
)

graph_interface = repo_for_test_gr_110.create_graph()
BaseValidator.graph_interface = graph_interface

validator = IsAgentixActionUsingExistingContentItemValidator()
results = validator.obtain_invalid_content_items_using_graph([action], False)

assert len(results) == 1
assert "unsupported in Agentix" in results[0].message


def test_gr110_builtin_command_skipped(repo_for_test_gr_110: Repo):
"""
Given:
- An Agentix Action referencing a built-in command.
When:
- Running the GR110 validator.
Then:
- No validation errors should be reported (built-in commands are skipped).
"""
action = create_agentix_action_object(
action_name="TestAction",
paths=[
"underlyingcontentitem.name",
"underlyingcontentitem.type",
"underlyingcontentitem.id",
],
values=[
"_builtin_",
"command",
"_builtin_",
],
)

graph_interface = repo_for_test_gr_110.create_graph()
BaseValidator.graph_interface = graph_interface

validator = IsAgentixActionUsingExistingContentItemValidator()
results = validator.obtain_invalid_content_items_using_graph([action], False)

assert len(results) == 0


def test_gr110_valid_command_reference(repo_for_test_gr_110: Repo, mocker):
"""
Given:
- An Agentix Action referencing an existing integration command.
When:
- Running the GR110 validator.
Then:
- No validation errors should be reported.
"""
action = create_agentix_action_object(
action_name="TestAction",
paths=[
"underlyingcontentitem.name",
"underlyingcontentitem.type",
"underlyingcontentitem.id",
"underlyingcontentitem.command",
],
values=[
"MyIntegration",
"command",
"MyIntegration",
"test-command",
],
)

graph_interface = repo_for_test_gr_110.create_graph()
BaseValidator.graph_interface = graph_interface

validator = IsAgentixActionUsingExistingContentItemValidator()

mock_command_node = mocker.Mock()
mocker.patch.object(validator.graph, "search", return_value=[mock_command_node])

results = validator.obtain_invalid_content_items_using_graph([action], False)

assert len(results) == 1


def test_gr110_valid_script_reference(repo_for_test_gr_110: Repo, mocker):
"""
Given:
- An Agentix Action referencing an existing script.
When:
- Running the GR110 validator.
Then:
- No validation errors should be reported.
"""
action = create_agentix_action_object(
action_name="TestAction",
paths=[
"underlyingcontentitem.name",
"underlyingcontentitem.type",
"underlyingcontentitem.id",
"underlyingcontentitem.command",
],
values=[
"MyScript",
"script",
"MyScript",
"",
],
)

graph_interface = repo_for_test_gr_110.create_graph()
BaseValidator.graph_interface = graph_interface

validator = IsAgentixActionUsingExistingContentItemValidator()
mock_script_node = mocker.Mock()
mocker.patch.object(validator.graph, "search", return_value=[mock_script_node])

results = validator.obtain_invalid_content_items_using_graph([action], False)

assert len(results) == 0


def test_gr110_valid_playbook_reference(repo_for_test_gr_110: Repo, mocker):
"""
Given:
- An Agentix Action referencing an existing playbook.
When:
- Running the GR110 validator.
Then:
- No validation errors should be reported.
"""
action = create_agentix_action_object(
action_name="TestAction",
paths=[
"underlyingcontentitem.name",
"underlyingcontentitem.type",
"underlyingcontentitem.id",
"underlyingcontentitem.command",
],
values=["MyPlaybook", "playbook", "MyPlaybook", ""],
)

graph_interface = repo_for_test_gr_110.create_graph()
BaseValidator.graph_interface = graph_interface

validator = IsAgentixActionUsingExistingContentItemValidator()
mock_playbook_node = mocker.Mock()
mocker.patch.object(validator.graph, "search", return_value=[mock_playbook_node])

results = validator.obtain_invalid_content_items_using_graph([action], False)

assert len(results) == 0
Loading