diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 954616acfd25..9f3b01f7f911 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -624,6 +624,39 @@ 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, + ) + 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, + resp_type="response" + ) + + def get_case_artifacts(self, case_id: str): + """ + API Endpoint: GET /public_api/v1/case/artifacts + """ + res = self._http_request( + method="GET", + url_suffix=f"/case/artifacts/{case_id}", + ) + return res + def extract_paths_and_names(paths: list) -> tuple: """ @@ -1878,6 +1911,156 @@ 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. + + 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"), "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")) 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: + 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_progress", "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), + "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": request_data}) + + readable_output = tableToMarkdown( + name="Cortex XDR Cases", + t=cases, + date_fields=["creation_time", "modification_time"], + 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. + + 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") # 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_progress=status, + resolve_reason=resolve_reason, + resolve_comment=resolve_comment, + ) + + client.update_case(case_id, request_data={"request_data": {"update_data": 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. + 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") + 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", []) + + 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( + name=f"Network Artifacts for Case {case_id}", + t=network_artifacts, + headerTransform=string_to_table_header, + removeNull=True, + ) + if file_artifacts: + readable_output += tableToMarkdown( + name=f"File Artifacts for Case {case_id}", + t=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 +2558,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..b5289eee0350 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4132,6 +4132,133 @@ 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_progress + description: The progress status of the case (e.g., New, Under Investigation). + type: String + - 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. if the status is updated to "resolved", resolution_reason must be provided. + 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