diff --git a/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.py b/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.py index e0c2177dbd9..53a8c4b5156 100644 --- a/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.py +++ b/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.py @@ -62,6 +62,7 @@ "core-list-scripts", "core-run-script-agentix", "core-list-endpoints", + "core-get-case-ai-summary", "core-list-exception-rules", "core-update-case", "core-get-endpoint-update-version", @@ -1073,6 +1074,22 @@ def get_custom_fields_metadata(self) -> dict[str, Any]: return self.get_webapp_data(request_data) + def get_case_ai_summary(self, case_id: int) -> dict: + """ + Retrieves AI-generated summary for a specific case ID. + + Args: + case_id (int): The ID of the case to retrieve AI summary for. + + Returns: + dict: API response containing case AI summary. + """ + return self._http_request( + method="POST", + url_suffix="/cases/get_ai_case_details", + json_data={"case_id": case_id}, + ) + def get_appsec_suggestion(client: Client, issue: dict, issue_id: str) -> dict: """ @@ -1640,8 +1657,7 @@ def get_case_extra_data(client, args): """ demisto.debug(f"Calling core-get-case-extra-data, {args=}") # Set the base URL for this API call to use the public API v1 endpoint - client._base_url = "api/webapp/public_api/v1" - case_extra_data = get_extra_data_for_case_id_command(client, args).outputs + case_extra_data = get_extra_data_for_case_id_command(init_client("public"), args).outputs demisto.debug(f"After calling core-get-case-extra-data, {case_extra_data=}") issue_ids = extract_ids(case_extra_data) case_data = case_extra_data.get("case", {}) @@ -1748,26 +1764,7 @@ def map_case_format(case_list): return mapped_cases -def get_cases_command(client, args): - """ - Retrieves cases from Cortex platform based on provided filtering criteria. - - Args: - client: The Cortex platform client instance for making API requests. - args (dict): Dictionary containing filter parameters including page number, - limits, time ranges, status, severity, and other case attributes. - - Returns: - List of mapped case objects containing case details and metadata. - """ - page = arg_to_number(args.get("page")) or 0 - limit = arg_to_number(args.get("limit")) or MAX_GET_CASES_LIMIT - - limit = page * MAX_GET_CASES_LIMIT + limit - page = page * MAX_GET_CASES_LIMIT - - sort_by_modification_time = args.get("sort_by_modification_time") - sort_by_creation_time = args.get("sort_by_creation_time") +def build_get_cases_filter(args: dict) -> FilterBuilder: since_creation_start_time = args.get("since_creation_time") since_creation_end_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") if since_creation_start_time else None since_modification_start_time = args.get("since_modification_time") @@ -1777,8 +1774,6 @@ def get_cases_command(client, args): gte_modification_time = args.get("gte_modification_time") lte_modification_time = args.get("lte_modification_time") - sort_field, sort_order = get_cases_sort_order(sort_by_creation_time, sort_by_modification_time) - status_values = [CaseManagement.STATUS[status] for status in argToList(args.get("status"))] severity_values = [CaseManagement.SEVERITY[severity] for severity in argToList(args.get("severity"))] tag_values = [CaseManagement.TAGS.get(tag, tag) for tag in argToList(args.get("tag"))] @@ -1852,9 +1847,64 @@ def get_cases_command(client, args): }, ) + return filter_builder + + +def add_cases_ai_summary(client, cases_list): + """ + Enriches a list of cases with AI-generated summaries. + + For each case in the provided list, fetches an AI-generated summary using the case ID + and updates the case's 'description' and 'name' fields with the AI-generated values. + + Args: + client: The API client instance used to fetch AI summaries + cases_list (list): List of case dictionaries, each containing at least a 'case_id' field + + Returns: + list: The enriched cases list with updated 'description' and 'name' fields where available. + Returns the original list if cases_list is None or empty. + + Note: + - If fetching AI summary fails for a case, that case remains unchanged + """ + try: + for case in cases_list: + case_id = case.get("case_id") + case_summary = get_case_ai_summary_command(client, {"case_id": case_id}) + if case_summary: + if case_description := case_summary.outputs.get("case_description"): # type: ignore + case["description"] = case_description + if case_name := case_summary.outputs.get("case_name"): # type: ignore + case["name"] = case_name + except Exception as e: + demisto.debug(str(e)) + return cases_list + + +def get_cases_command(client, args): + """ + Retrieves cases from Cortex platform based on provided filtering criteria. + + Args: + client: The Cortex platform client instance for making API requests. + args (dict): Dictionary containing filter parameters including page number, + limits, time ranges, status, severity, and other case attributes. + + Returns: + List of mapped case objects containing case details and metadata. + """ + + page = arg_to_number(args.get("page")) or 0 + limit = arg_to_number(args.get("limit")) or MAX_GET_CASES_LIMIT + + limit = page * MAX_GET_CASES_LIMIT + limit + page = page * MAX_GET_CASES_LIMIT + + sort_field, sort_order = get_cases_sort_order(args.get("sort_by_creation_time"), args.get("sort_by_modification_time")) request_data = build_webapp_request_data( table_name=CASES_TABLE, - filter_dict=filter_builder.to_dict(), + filter_dict=build_get_cases_filter(args).to_dict(), limit=limit, sort_field=sort_field, sort_order=sort_order, @@ -1871,14 +1921,15 @@ def get_cases_command(client, args): filter_count = int(reply.get("FILTER_COUNT", "0")) returned_count = len(data) - command_results = [] - - command_results.append( + command_results = [ CommandResults( outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CasesMetadata", outputs={"filter_count": filter_count, "returned_count": returned_count}, ) - ) + ] + + if filter_count == 1 and returned_count == 1 and data[0].get("issue_count") > 1: + data = add_cases_ai_summary(client, data) get_enriched_case_data = argToBoolean(args.get("get_enriched_case_data", "false")) # In case enriched case data was requested @@ -4071,6 +4122,91 @@ def xql_query_platform_command(client: Client, args: dict) -> CommandResults: ) +def get_case_ai_summary_command(client: Client, args: dict) -> CommandResults: + """ + Retrieves AI-generated summary for a specific case ID. + + Args: + client (Client): The client instance used to send the request. + args (dict): Dictionary containing the arguments for the command. + Expected to include: + - case_id (str): The ID of the case to retrieve AI summary for. + + Returns: + CommandResults: Object containing the formatted AI summary data, + raw response, and outputs for integration context. + """ + case_id = arg_to_number(args.get("case_id")) + if case_id is None: + raise DemistoException("get_case_ai_summary_command: case_id is required.") + response = client.get_case_ai_summary(case_id) + if not response: + raise DemistoException(f"Failed to fetch ai summary for case {case_id}. Ensure the asset ID is valid.") + + reply = response.get("reply", {}) + + output = { + "case_id": reply.get("case_id"), + "case_name": reply.get("case_name"), + "case_description": reply.get("case_description"), + } + + return CommandResults( + readable_output=tableToMarkdown("Case AI Summary", output, headerTransform=string_to_table_header), + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CaseAISummary", + outputs_key_field="case_id", + outputs=output, + raw_response=response, + ) + + +def init_client(api_type: str) -> Client: + """ + Initializes the Client for a specific API type. + + Args: + api_type (str): The category of the API (e.g., 'public', 'webapp', 'data_platform', etc.) + """ + params = demisto.params() + + # Connection parameters + proxy = params.get("proxy", False) + verify_cert = not params.get("insecure", False) + + try: + timeout = int(params.get("timeout", 120)) + except (ValueError, TypeError): + timeout = 120 + + # Base URL Mapping logic based on api_type + webapp_root = "/api/webapp" + + url_map = { + "webapp": webapp_root, + "public": f"{webapp_root}/public_api/v1", + "data_platform": f"{webapp_root}/data-platform", + "appsec": f"{webapp_root}/public_api/appsec", + "xsoar": "/xsoar", + "agents": f"{webapp_root}/agents", + } + + # Fallback to public API if the type isn't recognized + client_url = url_map.get(api_type, url_map["public"]) + + headers: dict = {"Authorization": params.get("api_key"), "Content-Type": "application/json"} + + return Client( + base_url=client_url, + proxy=proxy, + verify=verify_cert, + headers=headers, + timeout=timeout, + ) + +def verify_platform_version(version : str = "8.13.0"): + if not is_demisto_version_ge(version): + raise DemistoException("This command is not available for this platform version") + def main(): # pragma: no cover """ Executes an integration command @@ -4081,41 +4217,21 @@ def main(): # pragma: no cover args["integration_context_brand"] = INTEGRATION_CONTEXT_BRAND args["integration_name"] = INTEGRATION_NAME remove_nulls_from_dictionary(args) - headers: dict = {} - - webapp_api_url = "/api/webapp" - public_api_url = f"{webapp_api_url}/public_api/v1" - data_platform_api_url = f"{webapp_api_url}/data-platform" - appsec_api_url = f"{webapp_api_url}/public_api/appsec" - agents_api_url = f"{webapp_api_url}/agents" - xsoar_api_url = "/xsoar" - proxy = demisto.params().get("proxy", False) - verify_cert = not demisto.params().get("insecure", False) - try: - timeout = int(demisto.params().get("timeout", 120)) - except ValueError as e: - demisto.debug(f"Failed casting timeout parameter to int, falling back to 120 - {e}") - timeout = 120 - - client_url = public_api_url + # Logic to determine which API type the current command belongs to if command in WEBAPP_COMMANDS: - client_url = webapp_api_url + api_type = "webapp" elif command in DATA_PLATFORM_COMMANDS: - client_url = data_platform_api_url + api_type = "data_platform" elif command in APPSEC_COMMANDS: - client_url = appsec_api_url + api_type = "appsec" elif command in ENDPOINT_COMMANDS: - client_url = agents_api_url + api_type = "agents" elif command in XSOAR_COMMANDS: - client_url = xsoar_api_url + api_type = "xsoar" + else: + api_type = "public" - client = Client( - base_url=client_url, - proxy=proxy, - verify=verify_cert, - headers=headers, - timeout=timeout, - ) + client = init_client(api_type) try: if command == "test-module": @@ -4214,11 +4330,13 @@ def main(): # pragma: no cover return_results(update_endpoint_version_command(client, args)) elif command == "core-xql-generic-query-platform": - if not is_demisto_version_ge("8.13.0"): - raise DemistoException("This command is not available for this platform version") - + verify_platform_version() return_results(xql_query_platform_command(client, args)) + elif command == "core-get-case-ai-summary": + verify_platform_version() + return_results(get_case_ai_summary_command(client, args)) + except Exception as err: demisto.error(traceback.format_exc()) return_error(str(err)) diff --git a/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.yml b/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.yml index bf4c0c636f9..f2140be7160 100644 --- a/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.yml +++ b/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore.yml @@ -1356,6 +1356,24 @@ script: - contextPath: Core.CaseExtraData.file_artifacts.data.type description: The type of the file artifact. type: String + - arguments: + - description: The ID of the case to retrieve AI summary for. + name: case_id + required: true + type: number + name: core-get-case-ai-summary + description: Retrieves an AI-generated summary for a specific case. + hidden: true + outputs: + - contextPath: Core.CaseAISummary.case_id + description: The unique identifier for the case. + type: Number + - contextPath: Core.CaseAISummary.case_name + description: The name of the case. + type: String + - contextPath: Core.CaseAISummary.case_description + description: The AI-generated description of the case. + type: String - arguments: - description: Issue ID to update. If empty, updates the current issue ID. name: id diff --git a/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore_test.py b/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore_test.py index 7b35a452881..e809da39c1f 100644 --- a/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore_test.py +++ b/Packs/Core/Integrations/CortexPlatformCore/CortexPlatformCore_test.py @@ -4599,7 +4599,6 @@ def test_get_case_extra_data_with_all_fields_present(mocker): args = {"case_id": "123"} result = get_case_extra_data(mock_client, args) - assert mock_client._base_url == "api/webapp/public_api/v1" assert result["issue_ids"] == ["issue1", "issue2"] assert result["network_artifacts"] == [{"id": "net1", "type": "ip"}] assert result["file_artifacts"] == [{"id": "file1", "hash": "abc123"}] @@ -4610,33 +4609,6 @@ def test_get_case_extra_data_with_all_fields_present(mocker): assert result["detection_time"] == "2023-01-01T00:00:00Z" -def test_get_case_extra_data_client_base_url_modification(mocker): - """ - Given: - A mock client with an original base URL. - When: - The get_case_extra_data function is called. - Then: - The client's base URL should be modified to "api/webapp/public_api/v1". - """ - from CortexPlatformCore import get_case_extra_data - - mock_client = mocker.Mock() - original_url = "https://original.api.endpoint" - mock_client._base_url = original_url - - mock_command_result = mocker.Mock() - mock_command_result.outputs = {} - - mocker.patch("CortexPlatformCore.get_extra_data_for_case_id_command", return_value=mock_command_result) - mocker.patch("CortexPlatformCore.extract_ids", return_value=[]) - - args = {"case_id": "url_test"} - get_case_extra_data(mock_client, args) - - assert mock_client._base_url == "api/webapp/public_api/v1" - - def test_add_cases_extra_data_single_case(mocker): """ Given: diff --git a/Packs/Core/ReleaseNotes/3_4_83.md b/Packs/Core/ReleaseNotes/3_4_83.md new file mode 100644 index 00000000000..e7c6d758f64 --- /dev/null +++ b/Packs/Core/ReleaseNotes/3_4_83.md @@ -0,0 +1,15 @@ +<~PLATFORM> + +#### Integrations + +##### Cortex Platform - Core + +- Documentation and metadata improvements. + + + \ No newline at end of file diff --git a/Packs/Core/pack_metadata.json b/Packs/Core/pack_metadata.json index 850767b459d..953e5b5908c 100644 --- a/Packs/Core/pack_metadata.json +++ b/Packs/Core/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Core", "description": "Automates incident response", "support": "xsoar", - "currentVersion": "3.4.82", + "currentVersion": "3.4.83", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",