From 2197dd21baef099de8bbab34f3300c487d99dad5 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:56:25 +0200 Subject: [PATCH 1/3] Added the commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 168 ++++++++++++++++++ .../Integrations/CortexXDRIR/CortexXDRIR.yml | 94 ++++++++++ 2 files changed, 262 insertions(+) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 954616acfd25..36560bf3a4c8 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -624,6 +624,38 @@ def update_asset_group(self, group_id: str, request_data: dict): json_data=request_data, ) + def search_cases(self, request_data: dict): + """ + API Endpoint: POST /public_api/v1/case/search + """ + res = self._http_request( + method="POST", + url_suffix="/case/search", + json_data={"request_data": request_data}, + ) + return res.get("reply", {}).get("DATA", []) + + def update_case(self, case_id: str, request_data: dict): + """ + API Endpoint: POST /public_api/v1/case/update/{case-id} + """ + return self._http_request( + method="POST", + url_suffix=f"/case/update/{case_id}", + json_data={"request_data": request_data}, + ) + + def get_case_artifacts(self, case_id: str): + """ + API Endpoint: GET /public_api/v1/case/artifacts + """ + res = self._http_request( + method="GET", + url_suffix="/case/artifacts", + params={"case_id": case_id}, + ) + return res.get("reply", {}).get("DATA", {}) + def extract_paths_and_names(paths: list) -> tuple: """ @@ -1878,6 +1910,133 @@ def update_asset_group_command(client: Client, args: Dict) -> CommandResults: return CommandResults(readable_output="Asset group updated successfully") +def case_list_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Retrieve-cases-based-on-filters + Returns a list of cases. + """ + case_ids = argToList(args.get("case_id")) + case_domains = argToList(args.get("case_domain")) + severities = argToList(args.get("severity")) + statuses = argToList(args.get("status")) + created_before = arg_to_timestamp(args.get("created_before")) if args.get("created_before") else None + created_after = arg_to_timestamp(args.get("created_after")) if args.get("created_after") else None + sort_field = args.get("sort_field") + sort_order = args.get("sort_order") + limit = arg_to_number(args.get("limit")) or 50 + page_size = arg_to_number(args.get("page_size")) or limit + page = arg_to_number(args.get("page")) or 0 + + filters = [] + if case_ids: + filters.append({"field": "case_id", "operator": "in", "value": case_ids}) + if case_domains: + filters.append({"field": "case_domain", "operator": "in", "value": case_domains}) + if severities: + filters.append({"field": "severity", "operator": "in", "value": severities}) + if statuses: + filters.append({"field": "status", "operator": "in", "value": statuses}) + if created_before: + filters.append({"field": "creation_time", "operator": "lte", "value": created_before}) + if created_after: + filters.append({"field": "creation_time", "operator": "gte", "value": created_after}) + + request_data: Dict[str, Any] = { + "search_from": page * page_size, + "search_to": min((page + 1) * page_size, (page * page_size) + limit), + } + if filters: + request_data["filters"] = filters + if sort_field: + request_data["sort"] = {"field": sort_field, "keyword": sort_order or "desc"} + + cases = client.search_cases(request_data) + + readable_output = tableToMarkdown( + name="Cortex XDR Cases", + t=cases, + headerTransform=string_to_table_header, + removeNull=True, + ) + + return CommandResults( + readable_output=readable_output, + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Case", + outputs_key_field="case_id", + outputs=cases, + raw_response=cases, + ) + + +def case_update_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Update-existing-case + Updates an existing case. + """ + case_id = args.get("case_id", "") + status = args.get("status") + resolve_reason = args.get("resolve_reason") + resolve_comment = args.get("resolve_comment") + + update_data = assign_params( + status=status, + resolve_reason=resolve_reason, + resolve_comment=resolve_comment, + ) + + client.update_case(case_id, update_data) + + return CommandResults(readable_output=f"Case {case_id} updated successfully") + + +def case_artifact_list_command(client: Client, args: Dict[str, Any]) -> CommandResults: + """ + API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Retrieve-Case-Artifacts-by-Case-Id + Retrieves artifacts for a specific case. + """ + case_id = args.get("case_id", "") + artifacts = client.get_case_artifacts(case_id) + + network_artifacts = artifacts.get("network_artifacts", {}).get("DATA", []) + file_artifacts = artifacts.get("file_artifacts", {}).get("DATA", []) + + for artifact in network_artifacts: + artifact["case_id"] = case_id + for artifact in file_artifacts: + artifact["case_id"] = case_id + + readable_output = "" + if network_artifacts: + readable_output += tableToMarkdown( + f"Network Artifacts for Case {case_id}", + network_artifacts, + headerTransform=string_to_table_header, + removeNull=True, + ) + if file_artifacts: + readable_output += tableToMarkdown( + f"File Artifacts for Case {case_id}", + file_artifacts, + headerTransform=string_to_table_header, + removeNull=True, + ) + + if not readable_output: + readable_output = f"No artifacts found for case {case_id}" + + outputs = {} + if network_artifacts: + outputs[f"{INTEGRATION_CONTEXT_BRAND}.CaseNetworkArtifact"] = network_artifacts + if file_artifacts: + outputs[f"{INTEGRATION_CONTEXT_BRAND}.CaseFileArtifact"] = file_artifacts + + return CommandResults( + readable_output=readable_output, + outputs=outputs, + raw_response=artifacts + ) + + def main(): # pragma: no cover """ Executes an integration command @@ -2375,6 +2534,15 @@ def main(): # pragma: no cover elif command == "xdr-api-key-delete": return_results(api_key_delete_command(client, args)) + elif command == "xdr-case-list": + return_results(case_list_command(client, args)) + + elif command == "xdr-case-update": + return_results(case_update_command(client, args)) + + elif command == "xdr-case-artifact-list": + return_results(case_artifact_list_command(client, args)) + except Exception as err: return_error(str(err)) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index a3d31dddb5df..fe7163ca2a1f 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4132,6 +4132,100 @@ script: name: membership_predicate_json description: Updates an asset group. name: xdr-asset-group-update + - arguments: + - description: A comma-separated list of case IDs. + isArray: true + name: case_id + - description: A comma-separated list of case domains. + isArray: true + name: case_domain + - description: A comma-separated list of severities. + isArray: true + name: severity + - description: Filters cases that were created before this date. Supports English expressions like "in a year". + name: created_before + - description: Filters cases that were created after this date. Supports English expressions like "in a year". + name: created_after + - description: A comma-separated list of statuses. + isArray: true + name: status + - auto: PREDEFINED + description: The field by which to sort the results. + name: sort_field + predefined: + - case_id + - severity + - creation_time + - auto: PREDEFINED + description: The order in which to sort the results. + name: sort_order + predefined: + - asc + - desc + - description: Maximum number of cases to return. + name: limit + - description: Page size for pagination. + name: page_size + - description: Page number for pagination. + name: page + description: Returns a list of cases based on filters. + name: xdr-case-list + outputs: + - contextPath: PaloAltoNetworksXDR.Case.case_id + description: The unique identifier of the case. + type: String + - contextPath: PaloAltoNetworksXDR.Case.case_name + description: The name of the case. + type: String + - contextPath: PaloAltoNetworksXDR.Case.severity + description: The severity of the case. + type: String + - contextPath: PaloAltoNetworksXDR.Case.status + description: The status of the case. + type: String + - contextPath: PaloAltoNetworksXDR.Case.creation_time + description: The timestamp when the case was created. + type: Date + - arguments: + - description: The ID of the case to update. + name: case_id + required: true + - auto: PREDEFINED + description: The status to set for the case. + name: status + predefined: + - new + - under_investigation + - resolved + - auto: PREDEFINED + description: The reason for resolving the case. + name: resolve_reason + predefined: + - resolved_known_issue + - resolved_duplicate + - resolved_false_positive + - resolved_other + - resolved_true_positive + - resolved_security_testing + - resolved_fixed + - resolved_dismissed + - description: A comment explaining the resolution. + name: resolve_comment + description: Updates an existing case. + name: xdr-case-update + - arguments: + - description: The ID of the case for which to retrieve artifacts. + name: case_id + required: true + description: Retrieves artifacts for a specific case. + name: xdr-case-artifact-list + outputs: + - contextPath: PaloAltoNetworksXDR.CaseNetworkArtifact.case_id + description: The ID of the case associated with the artifact. + type: String + - contextPath: PaloAltoNetworksXDR.CaseFileArtifact.case_id + description: The ID of the case associated with the artifact. + type: String dockerimage: demisto/python3:3.12.12.5490952 isfetch: true isfetch:xpanse: false From b0432d1a735fcc68cbe451dad588ff38678c667b Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 13:00:12 +0200 Subject: [PATCH 2/3] run pre commit --- Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 36560bf3a4c8..839576300b44 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -2030,11 +2030,7 @@ def case_artifact_list_command(client: Client, args: Dict[str, Any]) -> CommandR if file_artifacts: outputs[f"{INTEGRATION_CONTEXT_BRAND}.CaseFileArtifact"] = file_artifacts - return CommandResults( - readable_output=readable_output, - outputs=outputs, - raw_response=artifacts - ) + return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=artifacts) def main(): # pragma: no cover From 9ab4fe0f6e9ac4e172111ed4ffeaffe835a2c3c9 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 15:36:20 +0200 Subject: [PATCH 3/3] tested all commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 82 +++++++++++++------ .../Integrations/CortexXDRIR/CortexXDRIR.yml | 47 +++++++++-- 2 files changed, 95 insertions(+), 34 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 839576300b44..9f3b01f7f911 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -631,18 +631,20 @@ def search_cases(self, request_data: dict): res = self._http_request( method="POST", url_suffix="/case/search", - json_data={"request_data": request_data}, + json_data=request_data, ) return res.get("reply", {}).get("DATA", []) def update_case(self, case_id: str, request_data: dict): """ API Endpoint: POST /public_api/v1/case/update/{case-id} + In case of success the API returns 204 """ return self._http_request( method="POST", url_suffix=f"/case/update/{case_id}", - json_data={"request_data": request_data}, + json_data=request_data, + resp_type="response" ) def get_case_artifacts(self, case_id: str): @@ -651,10 +653,9 @@ def get_case_artifacts(self, case_id: str): """ res = self._http_request( method="GET", - url_suffix="/case/artifacts", - params={"case_id": case_id}, + url_suffix=f"/case/artifacts/{case_id}", ) - return res.get("reply", {}).get("DATA", {}) + return res def extract_paths_and_names(paths: list) -> tuple: @@ -1914,28 +1915,36 @@ def case_list_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Retrieve-cases-based-on-filters Returns a list of cases. + + Args: + - client (Client): The client to use for the request. + - args (dict): The command arguments. + + Returns: + - CommandResults: A CommandResults object. """ case_ids = argToList(args.get("case_id")) case_domains = argToList(args.get("case_domain")) severities = argToList(args.get("severity")) statuses = argToList(args.get("status")) - created_before = arg_to_timestamp(args.get("created_before")) if args.get("created_before") else None - created_after = arg_to_timestamp(args.get("created_after")) if args.get("created_after") else None + created_before = arg_to_timestamp(args.get("created_before"), "created_before") if args.get("created_before") else None + created_after = arg_to_timestamp(args.get("created_after"), "created_after") if args.get("created_after") else None sort_field = args.get("sort_field") sort_order = args.get("sort_order") - limit = arg_to_number(args.get("limit")) or 50 - page_size = arg_to_number(args.get("page_size")) or limit - page = arg_to_number(args.get("page")) or 0 + limit = arg_to_number(args.get("limit")) if args.get("limit") else 50 + page_size = arg_to_number(args.get("page_size")) if args.get("page_size") else limit + page = arg_to_number(args.get("page")) if args.get("page_size") else 0 filters = [] if case_ids: - filters.append({"field": "case_id", "operator": "in", "value": case_ids}) + converted_ids = list(map(int, case_ids)) + filters.append({"field": "case_id", "operator": "in", "value": converted_ids}) if case_domains: filters.append({"field": "case_domain", "operator": "in", "value": case_domains}) if severities: filters.append({"field": "severity", "operator": "in", "value": severities}) if statuses: - filters.append({"field": "status", "operator": "in", "value": statuses}) + filters.append({"field": "status_progress", "operator": "in", "value": statuses}) if created_before: filters.append({"field": "creation_time", "operator": "lte", "value": created_before}) if created_after: @@ -1944,17 +1953,19 @@ def case_list_command(client: Client, args: Dict[str, Any]) -> CommandResults: request_data: Dict[str, Any] = { "search_from": page * page_size, "search_to": min((page + 1) * page_size, (page * page_size) + limit), + "filters": filters, } - if filters: - request_data["filters"] = filters if sort_field: request_data["sort"] = {"field": sort_field, "keyword": sort_order or "desc"} + else: + request_data["sort"] = {} - cases = client.search_cases(request_data) + cases = client.search_cases({"request_data": request_data}) readable_output = tableToMarkdown( name="Cortex XDR Cases", t=cases, + date_fields=["creation_time", "modification_time"], headerTransform=string_to_table_header, removeNull=True, ) @@ -1972,19 +1983,26 @@ def case_update_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Update-existing-case Updates an existing case. + + Args: + - client (Client): The client to use for the request. + - args (dict): The command arguments. + + Returns: + - CommandResults: A CommandResults object. """ - case_id = args.get("case_id", "") - status = args.get("status") - resolve_reason = args.get("resolve_reason") - resolve_comment = args.get("resolve_comment") + case_id = args.get("case_id") # required + status = args.get("status").upper() if args.get("status") else None + resolve_reason = args.get("resolve_reason").upper() if args.get("resolve_reason") else None + resolve_comment = args.get("resolve_comment").upper() if args.get("resolve_comment") else None update_data = assign_params( - status=status, + status_progress=status, resolve_reason=resolve_reason, resolve_comment=resolve_comment, ) - client.update_case(case_id, update_data) + client.update_case(case_id, request_data={"request_data": {"update_data": update_data}}) return CommandResults(readable_output=f"Case {case_id} updated successfully") @@ -1993,9 +2011,17 @@ def case_artifact_list_command(client: Client, args: Dict[str, Any]) -> CommandR """ API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Retrieve-Case-Artifacts-by-Case-Id Retrieves artifacts for a specific case. + Args: + - client (Client): The client to use for the request. + - args (dict): The command arguments. + + Returns: + - CommandResults: A CommandResults object. """ - case_id = args.get("case_id", "") + case_id = args.get("case_id") artifacts = client.get_case_artifacts(case_id) + if isinstance(artifacts, list): + artifacts = artifacts[0] network_artifacts = artifacts.get("network_artifacts", {}).get("DATA", []) file_artifacts = artifacts.get("file_artifacts", {}).get("DATA", []) @@ -2008,15 +2034,15 @@ def case_artifact_list_command(client: Client, args: Dict[str, Any]) -> CommandR readable_output = "" if network_artifacts: readable_output += tableToMarkdown( - f"Network Artifacts for Case {case_id}", - network_artifacts, + name=f"Network Artifacts for Case {case_id}", + t=network_artifacts, headerTransform=string_to_table_header, removeNull=True, ) if file_artifacts: readable_output += tableToMarkdown( - f"File Artifacts for Case {case_id}", - file_artifacts, + name=f"File Artifacts for Case {case_id}", + t=file_artifacts, headerTransform=string_to_table_header, removeNull=True, ) @@ -2030,7 +2056,9 @@ def case_artifact_list_command(client: Client, args: Dict[str, Any]) -> CommandR if file_artifacts: outputs[f"{INTEGRATION_CONTEXT_BRAND}.CaseFileArtifact"] = file_artifacts - return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=artifacts) + return CommandResults(readable_output=readable_output, + outputs=outputs, + raw_response=artifacts) def main(): # pragma: no cover diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index fe7163ca2a1f..b5289eee0350 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4180,18 +4180,51 @@ script: - contextPath: PaloAltoNetworksXDR.Case.severity description: The severity of the case. type: String - - contextPath: PaloAltoNetworksXDR.Case.status - description: The status of the case. + - contextPath: PaloAltoNetworksXDR.Case.status_progress + description: The progress status of the case (e.g., New, Under Investigation). type: String - - contextPath: PaloAltoNetworksXDR.Case.creation_time - description: The timestamp when the case was created. - type: Date + - contextPath: PaloAltoNetworksXDR.Case.description + description: A detailed description of the case and involved entities. + type: String + - contextPath: PaloAltoNetworksXDR.Case.low_severity_issue_count + description: The number of low severity issues associated with the case. + type: Number + - contextPath: PaloAltoNetworksXDR.Case.med_severity_issue_count + description: The number of medium severity issues associated with the case. + type: Number + - contextPath: PaloAltoNetworksXDR.Case.case_domain + description: The security domain of the case. + type: String + - contextPath: PaloAltoNetworksXDR.Case.xdr_url + description: The direct URL to the incident view in the XDR console. + type: String + - contextPath: PaloAltoNetworksXDR.Case.is_blocked + description: Indicates if the threat was blocked. + type: Boolean + - contextPath: PaloAltoNetworksXDR.Case.aggregated_score + description: The overall risk score calculated for the case. + type: Number + - contextPath: PaloAltoNetworksXDR.Case.host_count + description: The number of hosts involved in the case. + type: Number + - contextPath: PaloAltoNetworksXDR.Case.user_count + description: The number of users involved in the case. + type: Number + - contextPath: PaloAltoNetworksXDR.Case.wildfire_hits + description: The number of WildFire malware hits associated with the case. + type: Number + - contextPath: PaloAltoNetworksXDR.Case.tags + description: A list of tags associated with the case. + type: String + - contextPath: PaloAltoNetworksXDR.Case.starred + description: Whether the case has been starred/flagged. + type: Boolean - arguments: - description: The ID of the case to update. name: case_id required: true - auto: PREDEFINED - description: The status to set for the case. + description: The status to set for the case. if the status is updated to "resolved", resolution_reason must be provided. name: status predefined: - new @@ -4204,7 +4237,7 @@ script: - resolved_known_issue - resolved_duplicate - resolved_false_positive - - resolved_other + - "Resolved - Other" - resolved_true_positive - resolved_security_testing - resolved_fixed