From 1e19385dc34025a770d218eba846218a7215c829 Mon Sep 17 00:00:00 2001 From: barryyosi-panw <158817412+barryyosi-panw@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:27:42 +0200 Subject: [PATCH] Ciac 8941/enhancement/cortex xdr flexible close reason mappings mirroring (#33140) * Enhancing Cortex XDR IR integration with custom close-reason mapping capability - XDR->XSOAR * XSOAR->XDR custom close-reason mapping support * lint * update RN * update RN * Updated release notes * Fixes * Unittests * Documentation * pre-commit fixes * unittests labeling * updated RN * autopep8 * autopep8 * Describing `comma_separated_mapping_to_dict()` * Updating RN * mypy * mypy * autopep8 * Update RN * yaml update * Bump pack from version Base to 1.33.38. * Update Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/Integrations/CortexXDRIR/README.md Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/ctf01/ReleaseNotes/1_0_10.md Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/ReleaseNotes/6_1_18.md Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/Integrations/CortexXDRIR/README.md Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Update Packs/CortexXDR/Integrations/CortexXDRIR/README.md Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> * Bump pack from version Core to 3.0.22. * Bump pack from version CortexXDR to 6.1.19. * PR & demo fixes * PR fixes * Testing test module * Bump pack from version CortexXDR to 6.1.20. --------- Co-authored-by: michal-dagan Co-authored-by: Content Bot Co-authored-by: ShirleyDenkberg <62508050+ShirleyDenkberg@users.noreply.github.com> --- .../CoreIRApiModule/CoreIRApiModule.py | 74 ++-- .../CoreIRApiModule/CoreIRApiModule_test.py | 82 +++- Packs/Base/ReleaseNotes/1_33_38.md | 6 + .../CommonServerPython/CommonServerPython.py | 41 ++ Packs/Base/pack_metadata.json | 2 +- Packs/Core/ReleaseNotes/3_0_22.md | 6 + Packs/Core/pack_metadata.json | 2 +- .../Integrations/CortexXDRIR/CortexXDRIR.py | 134 ++++++- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 18 +- .../CortexXDRIR/CortexXDRIR_description.md | 7 + .../CortexXDRIR/CortexXDRIR_test.py | 125 +++++- .../Integrations/CortexXDRIR/README.md | 95 +++-- .../test_data/resolved_incident_data.json | 360 ++++++++++++++++++ Packs/CortexXDR/ReleaseNotes/6_1_20.md | 6 + Packs/CortexXDR/pack_metadata.json | 2 +- Packs/ctf01/ReleaseNotes/1_0_10.md | 6 + Packs/ctf01/pack_metadata.json | 2 +- 17 files changed, 872 insertions(+), 96 deletions(-) create mode 100644 Packs/Base/ReleaseNotes/1_33_38.md create mode 100644 Packs/Core/ReleaseNotes/3_0_22.md create mode 100644 Packs/CortexXDR/Integrations/CortexXDRIR/test_data/resolved_incident_data.json create mode 100644 Packs/CortexXDR/ReleaseNotes/6_1_20.md create mode 100644 Packs/ctf01/ReleaseNotes/1_0_10.md diff --git a/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.py b/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.py index 23ef2b54b464..c3139c0eb59a 100644 --- a/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.py +++ b/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.py @@ -13,18 +13,18 @@ XSOAR_RESOLVED_STATUS_TO_XDR = { 'Other': 'resolved_other', - 'Duplicate': 'resolved_duplicate', + 'Duplicate': 'resolved_duplicate_incident', 'False Positive': 'resolved_false_positive', 'Resolved': 'resolved_true_positive', - 'Resolved - Security Testing': 'resolved_security_testing', + 'Security Testing': 'resolved_security_testing', } XDR_RESOLVED_STATUS_TO_XSOAR = { 'resolved_known_issue': 'Other', - 'resolved_duplicate': 'Duplicate', + 'resolved_duplicate_incident': 'Duplicate', 'resolved_false_positive': 'False Positive', 'resolved_true_positive': 'Resolved', - 'resolved_security_testing': 'Resolved - Security Testing', + 'resolved_security_testing': 'Security Testing', 'resolved_other': 'Other', 'resolved_auto': 'Resolved' } @@ -346,7 +346,7 @@ def get_endpoints(self, endpoints = reply.get('reply').get('endpoints', []) return endpoints - def set_endpoints_alias(self, filters: list[dict[str, str]], new_alias_name: str | None) -> dict: # pragma: no cover + def set_endpoints_alias(self, filters: list[dict[str, str]], new_alias_name: str | None) -> dict: # pragma: no cover """ This func is used to set the alias name of an endpoint. @@ -934,8 +934,7 @@ def get_endpoint_device_control_violations(self, endpoint_ids: list, type_of_vio ip_list: list, vendor: list, vendor_id: list, product: list, product_id: list, serial: list, - hostname: list, violation_ids: list, username: list) \ - -> Dict[str, Any]: + hostname: list, violation_ids: list, username: list) -> Dict[str, Any]: arg_list = {'type': type_of_violation, 'endpoint_id_list': endpoint_ids, 'ip_list': ip_list, @@ -1709,8 +1708,8 @@ def validate_args_scan_commands(args): 'and without any other filters. This may cause performance issues.\n' \ 'To scan/abort scan some of the endpoints, please use the filter arguments.' if all_: - if endpoint_id_list or dist_name or gte_first_seen or gte_last_seen or lte_first_seen or lte_last_seen \ - or ip_list or group_name or platform or alias or hostname: + if (endpoint_id_list or dist_name or gte_first_seen or gte_last_seen or lte_first_seen or lte_last_seen + or ip_list or group_name or platform or alias or hostname): raise Exception(err_msg) elif not endpoint_id_list and not dist_name and not gte_first_seen and not gte_last_seen \ and not lte_first_seen and not lte_last_seen and not ip_list and not group_name and not platform \ @@ -2849,13 +2848,44 @@ def handle_outgoing_incident_owner_sync(update_args): def handle_user_unassignment(update_args): if ('assigned_user_mail' in update_args and update_args.get('assigned_user_mail') in ['None', 'null', '', None]) \ - or ('assigned_user_pretty_name' in update_args - and update_args.get('assigned_user_pretty_name') in ['None', 'null', '', None]): + or ('assigned_user_pretty_name' in update_args + and update_args.get('assigned_user_pretty_name') in ['None', 'null', '', None]): update_args['unassign_user'] = 'true' update_args['assigned_user_mail'] = None update_args['assigned_user_pretty_name'] = None +def resolve_xdr_close_reason(xsoar_close_reason: str) -> str: + """ + Resolving XDR close reason from possible custom XSOAR->XDR close-reason mapping or default mapping. + :param xsoar_close_reason: XSOAR raw status/close reason e.g. 'False Positive'. + :return: XDR close-reason in snake_case format e.g. 'resolved_false_positive'. + """ + # Initially setting the close reason according to the default mapping. + xdr_close_reason = XSOAR_RESOLVED_STATUS_TO_XDR.get(xsoar_close_reason, 'Other') + # Reading custom XSOAR->XDR close-reason mapping. + custom_xsoar_to_xdr_close_reason_mapping = comma_separated_mapping_to_dict( + demisto.params().get("custom_xsoar_to_xdr_close_reason_mapping") + ) + + # Overriding default close-reason mapping if there exists a custom one. + if xsoar_close_reason in custom_xsoar_to_xdr_close_reason_mapping: + xdr_close_reason_candidate = custom_xsoar_to_xdr_close_reason_mapping[xsoar_close_reason] + # Transforming resolved close-reason into snake_case format with known prefix to match XDR status format. + demisto.debug( + f"resolve_xdr_close_reason XSOAR->XDR custom close-reason exists, using {xsoar_close_reason}={xdr_close_reason}") + xdr_close_reason_candidate = "resolved_" + "_".join(xdr_close_reason_candidate.lower().split(" ")) + + if xdr_close_reason_candidate not in XDR_RESOLVED_STATUS_TO_XSOAR: + demisto.debug("Warning: Provided XDR close-reason does not exist. Using default XDR close-reason mapping. ") + else: + xdr_close_reason = xdr_close_reason_candidate + else: + demisto.debug(f"resolve_xdr_close_reason using default mapping {xsoar_close_reason}={xdr_close_reason}") + + return xdr_close_reason + + def handle_outgoing_issue_closure(remote_args): incident_id = remote_args.remote_incident_id demisto.debug(f"handle_outgoing_issue_closure {incident_id=}") @@ -2866,13 +2896,13 @@ def handle_outgoing_issue_closure(remote_args): # force closing remote incident only if: # The XSOAR incident is closed # and the remote incident isn't already closed - if remote_args.inc_status == 2 and \ - current_remote_status not in XDR_RESOLVED_STATUS_TO_XSOAR and close_reason: - + if remote_args.inc_status == 2 and current_remote_status not in XDR_RESOLVED_STATUS_TO_XSOAR and close_reason: if close_notes := update_args.get('closeNotes'): demisto.debug(f"handle_outgoing_issue_closure {incident_id=} {close_notes=}") update_args['resolve_comment'] = close_notes - update_args['status'] = XSOAR_RESOLVED_STATUS_TO_XDR.get(close_reason, 'Other') + + xdr_close_reason = resolve_xdr_close_reason(close_reason) + update_args['status'] = xdr_close_reason demisto.debug(f"handle_outgoing_issue_closure Closing Remote incident {incident_id=} with status {update_args['status']}") @@ -3148,7 +3178,6 @@ def get_script_code_command(client: CoreClient, args: Dict[str, str]) -> Tuple[s requires_polling_arg=False # means it will always be default to poll, poll=true ) def script_run_polling_command(args: dict, client: CoreClient) -> PollResult: - if action_id := args.get('action_id'): response = client.get_script_execution_status(action_id) general_status = response.get('reply', {}).get('general_status') or '' @@ -3740,7 +3769,6 @@ def create_request_filters( def args_to_request_filters(args): - if set(args.keys()) & { # check if any filter argument was provided 'endpoint_id_list', 'dist_name', 'ip_list', 'group_name', 'platform', 'alias_name', 'isolate', 'hostname', 'status', 'first_seen_gte', 'first_seen_lte', 'last_seen_gte', 'last_seen_lte' @@ -3814,7 +3842,6 @@ def parse_risky_users_or_hosts(user_or_host_data: dict[str, Any], score_header: str, description_header: str ) -> dict[str, Any]: - reasons = user_or_host_data.get('reasons', []) return { id_header: user_or_host_data.get('id'), @@ -4046,13 +4073,14 @@ def list_risky_users_or_host_command(client: CoreClient, command: str, args: dic ValueError: If the API connection fails. """ + def _warn_if_module_is_disabled(e: DemistoException) -> None: if ( - e is not None - and e.res is not None - and e.res.status_code == 500 - and 'No identity threat' in str(e) - and "An error occurred while processing XDR public API" in e.message + e is not None + and e.res is not None + and e.res.status_code == 500 + and 'No identity threat' in str(e) + and "An error occurred while processing XDR public API" in e.message ): return_warning(f'Please confirm the XDR Identity Threat Module is enabled.\nFull error message: {e}', exit=True) diff --git a/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule_test.py b/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule_test.py index 44600411bb20..032a6dfd2fd5 100644 --- a/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule_test.py +++ b/Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule_test.py @@ -9,7 +9,7 @@ import demistomock as demisto from CommonServerPython import Common, tableToMarkdown, pascalToSpace, DemistoException -from CoreIRApiModule import CoreClient, handle_outgoing_issue_closure +from CoreIRApiModule import CoreClient, handle_outgoing_issue_closure, XSOAR_RESOLVED_STATUS_TO_XDR from CoreIRApiModule import add_tag_to_endpoints_command, remove_tag_from_endpoints_command, quarantine_files_command, \ isolate_endpoint_command, list_user_groups_command, parse_user_groups, list_users_command, list_roles_command, \ change_user_role_command, list_risky_users_or_host_command, enrich_error_message_id_group_role, get_incidents_command @@ -18,7 +18,6 @@ base_url='https://test_api.com/public_api/v1', headers={} ) - Core_URL = 'https://api.xdrurl.com' ''' HELPER FUNCTIONS ''' @@ -544,7 +543,7 @@ def test_allowlist_files_command_with_more_than_one_file(requests_mock): test_data = load_test_data('test_data/blocklist_allowlist_files_success.json') expected_command_result = { 'CoreApiModule.allowlist.added_hashes.fileHash(val.fileHash == obj.fileHash)': - test_data['multi_command_args']['hash_list'] + test_data['multi_command_args']['hash_list'] } requests_mock.post(f'{Core_URL}/public_api/v1/hash_exceptions/allowlist/', json=test_data['api_response']) @@ -859,13 +858,14 @@ def test_handle_outgoing_issue_closure_close_reason(mocker): """ from CoreIRApiModule import handle_outgoing_issue_closure from CommonServerPython import UpdateRemoteSystemArgs - remote_args = UpdateRemoteSystemArgs({'delta': {'assigned_user_mail': 'None', 'closeReason': 'Resolved - Security Testing'}, + remote_args = UpdateRemoteSystemArgs({'delta': {'assigned_user_mail': 'None', 'closeReason': 'Security Testing'}, 'status': 2, 'inc_status': 2, 'data': {'status': 'other'}}) request_data_log = mocker.patch.object(demisto, 'debug') handle_outgoing_issue_closure(remote_args) - assert "handle_outgoing_issue_closure Closing Remote incident incident_id=None with status resolved_security_testing" in request_data_log.call_args[ # noqa: E501 - 0][0] + assert "handle_outgoing_issue_closure Closing Remote incident incident_id=None with status resolved_security_testing" in \ + request_data_log.call_args[ # noqa: E501 + 0][0] def test_get_update_args_close_incident(): @@ -3168,8 +3168,8 @@ def test_endpoint_alias_change_command__no_filters(mocker): }, { "err_msg": "An error occurred while processing XDR public API - No endpoint " - "was found " - "for creating the requested action", + "was found " + "for creating the requested action", "status_code": 500, }, False, @@ -3444,7 +3444,7 @@ def test_parse_user_groups(data: dict[str, Any], expected_results: list[dict[str [ ({"group_names": "test"}, "Error: Group test was not found. Full error message: Group 'test' was not found"), ({"group_names": "test, test2"}, "Error: Group test was not found. Note: If you sent more than one group name, " - "they may not exist either. Full error message: Group 'test' was not found") + "they may not exist either. Full error message: Group 'test' was not found") ] ) def test_list_user_groups_command_raise_exception(mocker, test_data: dict[str, str], excepted_error: str): @@ -3844,3 +3844,67 @@ def test_handle_outgoing_issue_closure(args, expected_delta): remote_args = UpdateRemoteSystemArgs(args) handle_outgoing_issue_closure(remote_args) assert remote_args.delta == expected_delta + + +@pytest.mark.parametrize('custom_mapping, expected_resolved_status', + [ + ("Other=Other,Duplicate=Other,False Positive=False Positive,Resolved=True Positive", + ["resolved_other", "resolved_other", "resolved_false_positive", "resolved_true_positive", + "resolved_security_testing"]), + + ("Other=True Positive,Duplicate=Other,False Positive=False Positive,Resolved=True Positive", + ["resolved_true_positive", "resolved_other", "resolved_false_positive", + "resolved_true_positive", "resolved_security_testing"]), + + ("Duplicate=Other", ["resolved_other", "resolved_other", "resolved_false_positive", + "resolved_true_positive", "resolved_security_testing"]), + + # Expecting default mapping to be used when no mapping provided. + ("", list(XSOAR_RESOLVED_STATUS_TO_XDR.values())), + + # Expecting default mapping to be used when improper mapping is provided. + ("Duplicate=RANDOM1, Other=Random2", list(XSOAR_RESOLVED_STATUS_TO_XDR.values())), + + ("Random1=Duplicate Incident", list(XSOAR_RESOLVED_STATUS_TO_XDR.values())), + + # Expecting default mapping to be used when improper mapping *format* is provided. + ("Duplicate=Other False Positive=Other", list(XSOAR_RESOLVED_STATUS_TO_XDR.values())), + + # Expecting default mapping to be used for when improper key-value pair *format* is provided. + ("Duplicate=Other, False Positive=Other True Positive=Other, Other=True Positive", + ["resolved_true_positive", "resolved_other", "resolved_false_positive", + "resolved_true_positive", "resolved_security_testing"]), + + ], + ids=["case-1", "case-2", "case-3", "empty-case", "improper-input-case-1", "improper-input-case-2", + "improper-input-case-3", "improper-input-case-4"] + ) +def test_xsoar_to_xdr_flexible_close_reason_mapping(capfd, mocker, custom_mapping, expected_resolved_status): + """ + Given: + - A custom XSOAR->XDR close-reason mapping + - Expected resolved XDR status according to the custom mapping. + When + - Handling outgoing issue closure (handle_outgoing_issue_closure(...) executed). + Then + - The resolved XDR statuses match the expected statuses for all possible XSOAR close-reasons. + """ + from CoreIRApiModule import handle_outgoing_issue_closure + from CommonServerPython import UpdateRemoteSystemArgs + + mocker.patch.object(demisto, 'params', return_value={"mirror_direction": "Both", + "custom_xsoar_to_xdr_close_reason_mapping": custom_mapping}) + + all_xsoar_close_reasons = XSOAR_RESOLVED_STATUS_TO_XDR.keys() + for i, close_reason in enumerate(all_xsoar_close_reasons): + remote_args = UpdateRemoteSystemArgs({'delta': {'closeReason': close_reason}, + 'status': 2, + 'inc_status': 2, + 'data': {'status': 'other'} + }) + # Overcoming expected non-empty stderr test failures (Errors are submitted to stderr when improper mapping is provided). + with capfd.disabled(): + handle_outgoing_issue_closure(remote_args) + + assert remote_args.delta.get('status') + assert remote_args.delta['status'] == expected_resolved_status[i] diff --git a/Packs/Base/ReleaseNotes/1_33_38.md b/Packs/Base/ReleaseNotes/1_33_38.md new file mode 100644 index 000000000000..85672c1be7fd --- /dev/null +++ b/Packs/Base/ReleaseNotes/1_33_38.md @@ -0,0 +1,6 @@ + +#### Scripts + +##### CommonServerPython + +Added a utility function `comma_separated_mapping_to_dict` that gets a comma-separated mapping `key1=value1,key2=value2,...` and transforms it into a dictionary object. diff --git a/Packs/Base/Scripts/CommonServerPython/CommonServerPython.py b/Packs/Base/Scripts/CommonServerPython/CommonServerPython.py index fec4b76db7d0..b6c6f28fa2fa 100644 --- a/Packs/Base/Scripts/CommonServerPython/CommonServerPython.py +++ b/Packs/Base/Scripts/CommonServerPython/CommonServerPython.py @@ -11751,6 +11751,47 @@ def data_error_handler(res): demisto.updateModuleHealth({'{data_type}Pulled'.format(data_type=data_type): data_size}) +def comma_separated_mapping_to_dict(raw_text): + """ + Transforming a textual comma-separated mapping into a dictionary object. + + :type raw_text: ``str`` + :param raw_text: Comma-separated mapping e.g ('key1=value1', 'key2=value2', ...) + + :rtype: ``dict`` + :return: Validated dictionary of the raw mapping e.g {'key1': 'value1', 'key2': 'value2', ...} + """ + demisto.debug("comma_separated_mapping_to_dict " + ">> Resolving comma-separated input mapping: {raw_text}".format(raw_text=raw_text)) + + mapping_dict = {} # type: Dict[str, str] + # If a proper mapping was not provided, return an empty dict. + if not raw_text: + return mapping_dict + + key_value_pairs = raw_text.split(',') + + for pair in key_value_pairs: + # Trimming trailing whitespace + pair = pair.strip() + + try: + key, value = pair.split('=') + except ValueError: + demisto.error("Error: Invalid mapping was provided. " + "Expected comma-separated mapping of format `key1=value1, key2=value2, ...`") + key = value = '' + + if key in mapping_dict: + demisto.debug( + "comma_separated_mapping_to_dict " + "Warning: duplicate key provided for {key}: using latter value: {value}".format(key=key, value=value) + ) + mapping_dict[key] = value + demisto.debug("comma_separated_mapping_to_dict << Resolved mapping: {mapping_dict}".format(mapping_dict=mapping_dict)) + return mapping_dict + + ########################################### # DO NOT ADD LINES AFTER THIS ONE # ########################################### diff --git a/Packs/Base/pack_metadata.json b/Packs/Base/pack_metadata.json index 41773a554726..48ac238dc3e5 100644 --- a/Packs/Base/pack_metadata.json +++ b/Packs/Base/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Base", "description": "The base pack for Cortex XSOAR.", "support": "xsoar", - "currentVersion": "1.33.37", + "currentVersion": "1.33.38", "author": "Cortex XSOAR", "serverMinVersion": "6.0.0", "url": "https://www.paloaltonetworks.com/cortex", diff --git a/Packs/Core/ReleaseNotes/3_0_22.md b/Packs/Core/ReleaseNotes/3_0_22.md new file mode 100644 index 000000000000..561ae5c640bd --- /dev/null +++ b/Packs/Core/ReleaseNotes/3_0_22.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Investigation & Response + +Added support for flexible close-reason mapping in `handle_outgoing_issue_closure` in `CoreIRApiModule`. Does not affect this module. \ No newline at end of file diff --git a/Packs/Core/pack_metadata.json b/Packs/Core/pack_metadata.json index 7f07fb1ac701..78825a9bde93 100644 --- a/Packs/Core/pack_metadata.json +++ b/Packs/Core/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Core - Investigation and Response", "description": "Automates incident response", "support": "xsoar", - "currentVersion": "3.0.21", + "currentVersion": "3.0.22", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 36d26776c43e..a24d98a54df3 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -44,6 +44,9 @@ 'Both': 'Both' } +XSOAR_TO_XDR = "XSOAR -> XDR" +XDR_TO_XSOAR = "XDR -> XSOAR" + def convert_epoch_to_milli(timestamp): if timestamp is None: @@ -122,7 +125,6 @@ def filter_and_save_unseen_incident(incidents: List, limit: int, number_of_alrea class Client(CoreClient): - def __init__(self, base_url, proxy, verify, timeout, params=None): if not params: params = {} @@ -148,6 +150,56 @@ def test_module(self, first_fetch_time): else: raise + # XSOAR -> XDR + self.validate_custom_mapping(mapping=self._params.get("custom_xsoar_to_xdr_close_reason_mapping"), + direction=XSOAR_TO_XDR) + + # XDR -> XSOAR + self.validate_custom_mapping(mapping=self._params.get("custom_xdr_to_xsoar_close_reason_mapping"), + direction=XDR_TO_XSOAR) + + def validate_custom_mapping(self, mapping: str, direction: str): + """ Check validity of provided custom close-reason mappings. """ + + xdr_statuses_to_xsoar = [status.replace("resolved_", "").replace("_", " ").title() + for status in XDR_RESOLVED_STATUS_TO_XSOAR.keys()] + xsoar_statuses_to_xdr = list(XSOAR_RESOLVED_STATUS_TO_XDR.keys()) + + exception_message = ('Improper custom mapping ({direction}) provided: "{key_or_value}" is not a valid Cortex ' + '{xsoar_or_xdr} close-reason. Valid Cortex {xsoar_or_xdr} close-reasons are: {statuses}') + + def to_xdr_status(status): + return "resolved_" + "_".join(status.lower().split(" ")) + + custom_mapping = comma_separated_mapping_to_dict(mapping) + + valid_key = valid_value = True # If no mapping was provided. + + for key, value in custom_mapping.items(): + if direction == XSOAR_TO_XDR: + xdr_close_reason = to_xdr_status(value) + valid_key = key in XSOAR_RESOLVED_STATUS_TO_XDR + valid_value = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR + elif direction == XDR_TO_XSOAR: + xdr_close_reason = to_xdr_status(key) + valid_key = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR + valid_value = value in XSOAR_RESOLVED_STATUS_TO_XDR + + if not valid_key: + raise DemistoException( + exception_message.format(direction=direction, + key_or_value=key, + xsoar_or_xdr="XSOAR" if direction == XSOAR_TO_XDR else "XDR", + statuses=xsoar_statuses_to_xdr + if direction == XSOAR_TO_XDR else xdr_statuses_to_xsoar)) + elif not valid_value: + raise DemistoException( + exception_message.format(direction=direction, + key_or_value=value, + xsoar_or_xdr="XDR" if direction == XSOAR_TO_XDR else "XSOAR", + statuses=xdr_statuses_to_xsoar + if direction == XSOAR_TO_XDR else xsoar_statuses_to_xdr)) + def handle_fetch_starred_incidents(self, limit: int, page_number: int, request_data: dict) -> List: """ handles pagination and filter of starred incidents that were fetched. @@ -626,32 +678,73 @@ def handle_incoming_user_unassignment(incident_data): incident_data['owner'] = '' -def handle_incoming_closing_incident(incident_data): - incident_id = incident_data.get('incident_id') - demisto.debug(f'handle_incoming_closing_incident {incident_data=} {incident_id=}') +def resolve_xsoar_close_reason(xdr_close_reason: str): + """ + Resolving XSOAR close reason from possible custom XDR->XSOAR close-reason mapping or default mapping. + :param xdr_close_reason: XDR raw status/close reason e.g. 'resolved_false_positive'. + :return: XSOAR close reason. + """ + + # Check if incoming XDR close-reason has a non-default mapping to XSOAR close-reason. + if demisto.params().get("custom_xdr_to_xsoar_close_reason_mapping"): + custom_xdr_to_xsoar_close_reason_mapping = comma_separated_mapping_to_dict( + demisto.params().get("custom_xdr_to_xsoar_close_reason_mapping") + ) + # XDR raw status/close-reason is prefixed with 'resolved_' and is given in snake_case format, + # e.g. 'resolved_false_positive', whilst custom XDR->XSOAR close-reason mapping + # is using title case format e.g. 'False Positive', therefore we need to adapt it accordingly. + title_cased_xdr_close_reason = ( + xdr_close_reason.replace("resolved_", "").replace("_", " ").title() + ) + xsoar_close_reason = custom_xdr_to_xsoar_close_reason_mapping.get(title_cased_xdr_close_reason) + if xsoar_close_reason in XSOAR_RESOLVED_STATUS_TO_XDR: + demisto.debug( + f"XDR->XSOAR custom close-reason exists, using {xdr_close_reason}={xsoar_close_reason}" + ) + return xsoar_close_reason + + # Otherwise, we use default mapping. + xsoar_close_reason = XDR_RESOLVED_STATUS_TO_XSOAR.get(xdr_close_reason) + demisto.debug( + f"XDR->XSOAR custom close-reason does not exists, using default mapping {xdr_close_reason}={xsoar_close_reason}" + ) + return xsoar_close_reason + + +def handle_incoming_closing_incident(incident_data) -> dict: + incident_id = incident_data.get("incident_id") + demisto.debug(f"handle_incoming_closing_incident {incident_data=} {incident_id=}") closing_entry = {} # type: Dict - if incident_data.get('status') in XDR_RESOLVED_STATUS_TO_XSOAR: - demisto.debug(f"handle_incoming_closing_incident {incident_data.get('status')=} {incident_id=}") + + if incident_data.get("status") in XDR_RESOLVED_STATUS_TO_XSOAR: + demisto.debug( + f"handle_incoming_closing_incident {incident_data.get('status')=} {incident_id=}" + ) demisto.debug(f"Closing XDR issue {incident_id=}") + xsoar_close_reason = resolve_xsoar_close_reason(incident_data.get("status")) closing_entry = { - 'Type': EntryType.NOTE, - 'Contents': { - 'dbotIncidentClose': True, - 'closeReason': XDR_RESOLVED_STATUS_TO_XSOAR.get(incident_data.get("status")), - 'closeNotes': incident_data.get('resolve_comment', '') + "Type": EntryType.NOTE, + "Contents": { + "dbotIncidentClose": True, + "closeReason": xsoar_close_reason, + "closeNotes": incident_data.get("resolve_comment", ""), }, - 'ContentsFormat': EntryFormat.JSON + "ContentsFormat": EntryFormat.JSON, } - incident_data['closeReason'] = closing_entry['Contents']['closeReason'] - incident_data['closeNotes'] = closing_entry['Contents']['closeNotes'] - demisto.debug(f"handle_incoming_closing_incident {incident_id=} {incident_data['closeReason']=} " - f"{incident_data['closeNotes']=}") + incident_data["closeReason"] = closing_entry["Contents"]["closeReason"] + incident_data["closeNotes"] = closing_entry["Contents"]["closeNotes"] + demisto.debug( + f"handle_incoming_closing_incident {incident_id=} {incident_data['closeReason']=} " + f"{incident_data['closeNotes']=}" + ) - if incident_data.get('status') == 'resolved_known_issue': + if incident_data.get("status") == "resolved_known_issue": close_notes = f'Known Issue.\n{incident_data.get("closeNotes", "")}' - closing_entry['Contents']['closeNotes'] = close_notes - incident_data['closeNotes'] = close_notes - demisto.debug(f"handle_incoming_closing_incident {incident_id=} {close_notes=}") + closing_entry["Contents"]["closeNotes"] = close_notes + incident_data["closeNotes"] = close_notes + demisto.debug( + f"handle_incoming_closing_incident {incident_id=} {close_notes=}" + ) return closing_entry @@ -930,7 +1023,6 @@ def file_details_results(client: Client, args: Dict, add_to_context: bool) -> No def get_contributing_event_command(client: Client, args: Dict) -> CommandResults: - if alert_ids := argToList(args.get('alert_ids')): alerts = [] diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index f39c91278026..3132393f3017 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -36,6 +36,20 @@ configuration: - Incoming - Outgoing - Both +- display: Custom close-reason mapping (XSOAR -> XDR mirrored incident. Overwrites default close-reason mapping defined by Cortex XSOAR) + section: Collect + additionalinfo: 'Define how to close the mirrored incidents from Cortex XSOAR into Cortex XDR with a custom close reason mapping. Enter a comma-separated list of close reasons (acceptable format {Cortex XSOAR close reason}={Cortex XDR close reason}) to override the default close reason mapping defined by Cortex XSOAR. Note that the mapping must be configured accordingly with the existing close reasons in Cortex XSOAR and Cortex XDR. Not following this format will result in closing the incident with a default close reason. Example: "Resolved=Other,Duplicate=Other". Refer to integration documentation for possible close-reasons - `XDR Incident Mirroring`.' + name: custom_xsoar_to_xdr_close_reason_mapping + defaultvalue: '' + type: 0 + required: false +- display: Custom close-reason mapping (XDR -> XSOAR mirrored incident. Overwrites default close-reason mapping defined by Cortex XSOAR) + section: Collect + additionalinfo: 'Define how to close the mirrored incidents from Cortex XDR into Cortex XSOAR with a custom close reason mapping. Enter a comma-separated list of close reasons (acceptable format {Cortex XDR close reason}={Cortex XSOAR close reason}) to override the default close reason mapping defined by Cortex XSOAR. Note that the mapping must be configured accordingly with the existing close reasons in Cortex XSOAR and Cortex XDR. Not following this format will result in closing the incident with a default close reason. Example: “Known Issue=Resolved, Duplicate Incident=Other". Refer to integration documentation for possible close-reasons - `XDR Incident Mirroring`.' + name: custom_xdr_to_xsoar_close_reason_mapping + defaultvalue: '' + type: 0 + required: false - name: url type: 0 display: 'Server URL (copy URL from XDR)' @@ -241,7 +255,7 @@ script: type: String - contextPath: PaloAltoNetworksXDR.Incident.status description: | - Current status of the incident. Valid values are: "new","under_investigation","resolved_known_issue","resolved_duplicate","resolved_false_positive","resolved_true_positive","resolved_security_testing" or "resolved_other". + Current status of the incident. Valid values are: "new","under_investigation","resolved_known_issue","resolved_duplicate_incident","resolved_false_positive","resolved_true_positive","resolved_security_testing" or "resolved_other". type: String - contextPath: PaloAltoNetworksXDR.Incident.description description: Dynamic calculated description of the incident. @@ -3471,7 +3485,7 @@ script: isArray: true name: xdr-remove-user-role description: Remove one or more users from a role. - dockerimage: demisto/python3:3.10.13.87159 + dockerimage: demisto/python3:3.10.13.89009 isfetch: true isfetch:xpanse: false script: '' diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_description.md b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_description.md index 7f0c436a6a4b..9bd6257b4199 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_description.md +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_description.md @@ -13,4 +13,11 @@ 2. Click the **Copy URL** button in the top right corner. --- + +### Mirroring + +**Close-reason default mapping XSOAR -> XDR**: _Other=Other, Duplicate=Duplicate Incident, False Positive=False Positive, Resolved=True Positive_ + +**Close-reason default mapping XDR -> XSOAR**: _Known Issue=Other, Duplicate Incident=Duplicate, False Positive=False Positive, True Positive=Resolved, Other=Other, Auto=Resolved_ + [View Integration Documentation](https://xsoar.pan.dev/docs/reference/integrations/cortex-xdr---ir) \ No newline at end of file diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index 768cb6220203..38444fdf98ac 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -5,9 +5,9 @@ from freezegun import freeze_time import demistomock as demisto -from CommonServerPython import Common -from CortexXDRIR import XDR_RESOLVED_STATUS_TO_XSOAR - +from CommonServerPython import Common, urljoin, DemistoException +from CoreIRApiModule import XDR_RESOLVED_STATUS_TO_XSOAR +from CortexXDRIR import XSOAR_TO_XDR, XDR_TO_XSOAR XDR_URL = 'https://api.xdrurl.com' ''' HELPER FUNCTIONS ''' @@ -499,7 +499,7 @@ def test_get_remote_data_command_should_update(requests_mock, mocker): def test_get_remote_data_command_with_rate_limit_exception(mocker): """ Given: - - an XDR client + - an XDR client - arguments (id and lastUpdate time set to a lower than incident modification time) - a Rate limit exception is raises from get_extra_data_command method When @@ -792,3 +792,120 @@ def test_update_remote_system_command(incident_changed, delta): } actual_remote_id = update_remote_system_command(client, args) assert actual_remote_id == expected_remote_id + + +@pytest.mark.parametrize('custom_mapping, expected_resolved_status', + [ + ("Known Issue=Other,Duplicate Incident=Duplicate,False Positive=False Positive," + "True Positive=Resolved,Security Testing=Other,Other=Other", + ["Other", "Duplicate", "False Positive", "Resolved", "Other", "Other", "Resolved"]), + + ("Known Issue=Other,Duplicate Incident=Other,False Positive=False Positive," + "True Positive=Resolved,Security Testing=Other,Other=Other", + ["Other", "Other", "False Positive", "Resolved", "Other", "Other", "Resolved"]), + + ("Duplicate Incident=Other,Security Testing=Other,Other=Other", + ["Other", "Other", "False Positive", "Resolved", "Other", "Other", "Resolved"]), + + # Expecting default mapping to be used when no mapping provided. + ("", list(XDR_RESOLVED_STATUS_TO_XSOAR.values())), + + # Expecting default mapping to be used when improper mapping is provided. + ("Duplicate=RANDOM1, Other=Random2", list(XDR_RESOLVED_STATUS_TO_XSOAR.values())), + + ("Duplicate Incident=Random3", list(XDR_RESOLVED_STATUS_TO_XSOAR.values())), + + # Expecting default mapping to be used when improper mapping *format* is provided. + ("Duplicate Incident=Other False Positive=Other", list(XDR_RESOLVED_STATUS_TO_XSOAR.values())), + + # Expecting default mapping to be used for when improper key-value pair *format* is provided. + ("Duplicate Incident=Other, False Positive=Other True Positive=Other", + ["Other", "Other", "False Positive", "Resolved", "Security Testing", "Other", + "Resolved"]), + + ], + ids=["case-1", "case-2", "case-3", "empty-case", "improper-input-case-1", "improper-input-case-2", + "improper-input-case-3", "improper-input-case-4"] + ) +def test_xdr_to_xsoar_flexible_close_reason_mapping(capfd, mocker, custom_mapping, expected_resolved_status): + """ + Given: + - A custom XDR->XSOAR close-reason mapping + - Expected resolved XSOAR status according to the custom mapping. + When + - Handling incoming closing-incident (handle_incoming_closing_incident(...) executed). + Then + - The resolved XSOAR statuses match the expected statuses for all possible XDR close-reasons. + """ + from CortexXDRIR import handle_incoming_closing_incident + mocker.patch.object(demisto, 'params', return_value={"mirror_direction": "Both", + "custom_xdr_to_xsoar_close_reason_mapping": custom_mapping}) + + all_xdr_close_reasons = XDR_RESOLVED_STATUS_TO_XSOAR.keys() + + for i, xdr_close_reason in enumerate(all_xdr_close_reasons): + # Mock an xdr incident with "resolved" status. + incident_data = load_test_data('./test_data/resolved_incident_data.json') + # Set incident status to be tested close-reason. + incident_data["status"] = xdr_close_reason + + # Overcoming expected non-empty stderr test failures (Errors are submitted to stderr when improper mapping is provided). + with capfd.disabled(): + close_entry = handle_incoming_closing_incident(incident_data) + assert close_entry["Contents"]["closeReason"] == expected_resolved_status[i] + + +@pytest.mark.parametrize('custom_mapping, direction, should_raise_error', + [ + ("Other=Other,Duplicate=Other,False Positive=False Positive,Resolved=True Positive", + XSOAR_TO_XDR, False), + + ("Known Issue=Other,Duplicate Incident=Duplicate,False Positive=False Positive", + XDR_TO_XSOAR, False), + + ("Duplicate Incident=Random", XSOAR_TO_XDR, True), + + ("Duplicate=RANDOM1, Other=Random2", XDR_TO_XSOAR, True), + # Inverted map provided + ("Duplicate=Duplicate Incident", XDR_TO_XSOAR, True), + ("Duplicate Incident=Duplicate", XSOAR_TO_XDR, True), + # Improper mapping + ("Random1, Random2", XDR_TO_XSOAR, True), + ("Random1, Random2", XSOAR_TO_XDR, True), + + ], + ids=["case-1", "case-2", "case-3", "case-4", "case-5", "case-6", "case-7", "case-8"] + ) +def test_test_module(capfd, custom_mapping, direction, should_raise_error): + """ + Given: + - mock client with username and api_key (basic auth) + When: + - run `test_module` function + Then: + - Ensure no error is raised, and return `ok` + """ + from CortexXDRIR import Client + + # using two different credentials object as they both fields need to be encrypted + base_url = urljoin("dummy_url", '/public_api/v1') + proxy = demisto.params().get('proxy') + verify_cert = not demisto.params().get('insecure', False) + + client = Client( + base_url=base_url, + proxy=proxy, + verify=verify_cert, + timeout=120, + params=demisto.params() + ) + # Overcoming expected non-empty stderr test failures (Errors are submitted to stderr when improper mapping is provided). + with capfd.disabled(): + if should_raise_error: + with pytest.raises(DemistoException): + client.validate_custom_mapping(mapping=custom_mapping, direction=direction) + else: + try: + client.validate_custom_mapping(mapping=custom_mapping, direction=direction) + except DemistoException as e: + pytest.fail(f"Unexpected exception raised for input {input}: {e}") diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/README.md b/Packs/CortexXDR/Integrations/CortexXDRIR/README.md index 5b062d7d731e..f874137a1a2a 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/README.md +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/README.md @@ -7,26 +7,28 @@ This integration was integrated and tested with version 2.6.5 of Cortex XDR - IR 2. Search for Palo Alto Networks Cortex XDR - Investigation and Response. 3. Click **Add instance** to create and configure a new integration instance. - | **Parameter** | **Description** | **Required** | - | - | --- | --- | - | Fetch incidents | | False | - | Incident type | | False | - | Remove legacy incident fields | Unchecked for backwards compatibility, recommended to check. This will remove duplicated incident fields under file_artifacts, network_artifacts, and alerts (like client_id, clientid.) | False | - | Incident Mirroring Direction | | False | - | Server URL (copy URL from XDR - click ? to see more info.) | | True | - | API Key ID | | False | - | API Key | | False | - | HTTP Timeout | The timeout of the HTTP requests sent to Cortex XDR API \(in seconds\). | False | - | Maximum number of incidents per fetch | The maximum number of incidents per fetch. Cannot exceed 100. | False | - | Only fetch starred incidents | | False | - | Starred incidents fetch window | Starred fetch window timestamp \(&lt;number&gt; &lt;time unit&gt;, e.g., 12 hours, 7 days\). Fetches only starred incidents within the specified time range. | False | - | First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) | | False | - | Sync Incident Owners | For Cortex XSOAR version 6.0.0 and above. If selected, for every incident fetched from Cortex XDR to Cortex XSOAR, the incident owners will be synced. Note that once this value is changed and synchronized between the systems, additional changes will not be reflected. For example, if you change the owner in Cortex XSOAR, the new owner will also be changed in Cortex XDR. However, if you now change the owner back in Cortex XDR, this additional change will not be reflected in Cortex XSOAR. In addition, for this change to be reflected, the owners must exist in both Cortex XSOAR and Cortex XDR. | False | - | Trust any certificate (not secure) | | False | - | Use system proxy settings | | False | - | Prevent Only Mode | Whether the XDR tenant Mode is prevent only | False | - | Incident Statuses to Fetch | The statuses of the incidents that will be fetched. If no status is provided then incidents of all the statuses will be fetched. Note: An incident whose status was changed to a filtered status after its creation time will not be fetched. | False | - | Incidents Fetch Interval | | False | + | **Parameter** | **Description** | **Required** | + |----------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| + | Fetch incidents | | False | + | Incident type | | False | + | Remove legacy incident fields | Unchecked for backwards compatibility, recommended to check. This will remove duplicated incident fields under file_artifacts, network_artifacts, and alerts (like client_id, clientid.) | False | + | Incident Mirroring Direction | | False | + | Custom close-reason mapping for mirrored **XSOAR -> XDR** incidents. | Define how to close the mirrored incidents from Cortex XSOAR into XDR with a custom close reason mapping. Enter a comma-separated close-reason mapping (acceptable format {XSOAR close reason}={XDR close reason}) to override the default close reason mapping defined by XSOAR. Note that the mapping must be configured accordingly with the existing close reasons in Cortex XSOAR and XDR. Not following this format will result in closing the incident with a default close reason. Example: "Resolved=Other". Default: "Other=Other,Duplicate=Duplicate Incident,False Positive=False Positive,Resolved=True Positive”. Refer to integration documentation for possible close-reasons (`XDR Incident Mirroring, sec. 7`). | False | + | Custom close-reason mapping for mirrored **XDR -> XSOAR** incidents. | Define how to close the mirrored incidents from Cortex XDR into XSOAR with a custom close reason mapping. Enter a comma-separated list of close reasons (acceptable format {XDR close reason}={XSOAR close reason}) to override the default close reason mapping defined by XSOAR. Note that the mapping must be configured accordingly with the existing close reasons in Cortex XSOAR and XDR. Not following this format will result in closing the incident with a default close reason. Example: “Known Issue=Resolved". Default: “Known Issue=Other,Duplicate Incident=Duplicate,False Positive=False Positive,True Positive=Resolved,Security Testing=Other,Other=Other,Auto=Resolved". Refer to integration documentation for possible close-reasons (`XDR Incident Mirroring, sec. 7`). | False | + | Server URL (copy URL from XDR - click ? to see more info.) | | True | + | API Key ID | | False | + | API Key | | False | + | HTTP Timeout | The timeout of the HTTP requests sent to Cortex XDR API \(in seconds\). | False | + | Maximum number of incidents per fetch | The maximum number of incidents per fetch. Cannot exceed 100. | False | + | Only fetch starred incidents | | False | + | Starred incidents fetch window | Starred fetch window timestamp \(&lt;number&gt; &lt;time unit&gt;, e.g., 12 hours, 7 days\). Fetches only starred incidents within the specified time range. | False | + | First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) | | False | + | Sync Incident Owners | For Cortex XSOAR version 6.0.0 and above. If selected, for every incident fetched from Cortex XDR to Cortex XSOAR, the incident owners will be synced. Note that once this value is changed and synchronized between the systems, additional changes will not be reflected. For example, if you change the owner in Cortex XSOAR, the new owner will also be changed in Cortex XDR. However, if you now change the owner back in Cortex XDR, this additional change will not be reflected in Cortex XSOAR. In addition, for this change to be reflected, the owners must exist in both Cortex XSOAR and Cortex XDR. | False | + | Trust any certificate (not secure) | | False | + | Use system proxy settings | | False | + | Prevent Only Mode | Whether the XDR tenant Mode is prevent only | False | + | Incident Statuses to Fetch | The statuses of the incidents that will be fetched. If no status is provided then incidents of all the statuses will be fetched. Note: An incident whose status was changed to a filtered status after its creation time will not be fetched. | False | + | Incidents Fetch Interval | | False | 4. Click **Test** to validate the URLs, token, and connection. @@ -52,6 +54,8 @@ For builtin role with less permission but maximum command running abilities, use 1. In your Cortex XDR platform, go to **Settings** > **Configurations** > **API key** page > **API Keys**. 2. Click the **Copy URL** button in the top right corner. +#### XDR & XSOAR + ## Playbooks --- @@ -137,19 +141,44 @@ To setup the mirroring follow these instructions: 4. Under **Mapper (incoming)**, select `XDR - Incoming Mapper`. 5. Under **Mapper (outgoing)**, select `Cortex XDR - Outgoing Mapper`. 6. In the *Incident Mirroring Direction* integration parameter, select in which direction the incidents should be mirrored: - -- Incoming - Any changes in XDR incidents will be reflected in XSOAR incidents. -- Outgoing - Any changes in XSOAR incidents will be reflected in XDR incidents. -- Both - Changes in XSOAR and XDR incidents will be reflected in both directions. -- None - Choose this to turn off incident mirroring. - -7. Optional: Check the *Sync Incident Owners* integration parameter to sync the incident owners in both XDR and XSOAR. - -- Note: This feature will only work if the same users are registered in both Cortex XSOAR and Cortex XDR. - -8. Newly fetched incidents will be mirrored in the chosen direction. - -- Note: This will not effect existing incidents. + - Incoming - Any changes in Cortex XDR incidents will be reflected in Cortex XSOAR incidents. + - Outgoing - Any changes in Cortex XSOAR incidents will be reflected in Cortex XDR incidents. + - Both - Changes in Cortex XSOAR and Cortex XDR incidents will be reflected in both directions. + - None - Choose this to turn off incident mirroring. + +7. Optional: Provide a custom close-reason mapping for mirrored XDR <-> XSOAR incidents. Please use only possible close-reasons to map: + + | Possible Closure Reasons for Cortex XSOAR Incident | + |----------------------------------------------------| + | Resolved | + | False Positive | + | Duplicate | + | Security Testing | + | Other | + + |Possible Closure Reasons for Cortex Cortex XDR Incident| + |-----------------------------------| + | True Positive | + | False Positive | + | Duplicate Incident | + | Security Testing | + | Known Issue | + | Other | + | Auto | + + Failing to use only available values will result in using default mapping of closure reasons within the mirroring process. + + **Close-reason default mapping XSOAR -> XDR**: _Other=Other, Duplicate=Duplicate Incident, False Positive=False Positive, Resolved=True Positive_ + + **Close-reason default mapping XDR -> XSOAR**: _Known Issue=Other, Duplicate Incident=Duplicate, False Positive=False Positive, True Positive=Resolved, Other=Other, Auto=Resolved_ + +8. Optional: Check the *Sync Incident Owners* integration parameter to sync the incident owners in both XDR and XSOAR. + + - Note: This feature will only work if the same users are registered in both Cortex XSOAR and Cortex XDR. + +9. Newly fetched incidents will be mirrored in the chosen direction. + + - Note: This will not effect existing incidents. ### XDR Mirroring Notes, limitations and Troubleshooting diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/test_data/resolved_incident_data.json b/Packs/CortexXDR/Integrations/CortexXDRIR/test_data/resolved_incident_data.json new file mode 100644 index 000000000000..169723ceac0e --- /dev/null +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/test_data/resolved_incident_data.json @@ -0,0 +1,360 @@ +{ + "incident_id": "1", + "is_blocked": false, + "incident_name": null, + "modification_time": 1708940581625, + "detection_time": null, + "status": "resolved_duplicate_incident", + "severity": "medium", + "description": " --- ", + "assigned_user_mail": "", + "assigned_user_pretty_name": "", + "alert_count": 1, + "low_severity_alert_count": 0, + "med_severity_alert_count": 1, + "high_severity_alert_count": 0, + "critical_severity_alert_count": 0, + "user_count": 0, + "host_count": 0, + "notes": null, + "resolve_comment": null, + "resolved_timestamp": 1708940581625, + "manual_severity": null, + "manual_description": null, + "xdr_url": "https://demisto.hello.com/incident-view/1", + "starred": false, + "starred_manually": false, + "hosts": null, + "users": [], + "incident_sources": [ + "Correlation" + ], + "rule_based_score": null, + "predicted_score": null, + "manual_score": null, + "aggregated_score": null, + "wildfire_hits": 0, + "alerts_grouping_status": "Disabled", + "mitre_tactics_ids_and_names": null, + "mitre_techniques_ids_and_names": null, + "alert_categories": [ + "Other" + ], + "original_tags": [ + "DS:Microsoft Graph" + ], + "tags": [ + "DS:Microsoft Graph" + ], + "alerts": [ + { + "agent_os_sub_type": null, + "fw_app_category": null, + "fw_app_id": null, + "fw_app_subcategory": null, + "fw_app_technology": null, + "category": "Other", + "causality_actor_process_command_line": null, + "causality_actor_process_image_md5": null, + "causality_actor_process_image_name": null, + "causality_actor_process_image_path": null, + "causality_actor_process_image_sha256": null, + "causality_actor_process_signature_status": "N/A", + "causality_actor_process_signature_vendor": null, + "causality_actor_causality_id": null, + "identity_sub_type": null, + "identity_type": null, + "operation_name": null, + "project": null, + "cloud_provider": null, + "referenced_resource": null, + "resource_sub_type": null, + "resource_type": null, + "cluster_name": null, + "container_id": null, + "contains_featured_host": "NO", + "contains_featured_ip": "NO", + "contains_featured_user": "NO", + "action_country": "UNKNOWN", + "fw_interface_to": null, + "dns_query_name": null, + "agent_device_domain": null, + "fw_email_recipient": null, + "fw_email_sender": null, + "fw_email_subject": null, + "event_type": null, + "is_whitelisted": false, + "action_file_macro_sha256": null, + "action_file_md5": null, + "action_file_name": null, + "action_file_path": null, + "action_file_sha256": null, + "fw_device_name": null, + "fw_rule_id": null, + "fw_rule": null, + "fw_serial_number": null, + "agent_fqdn": null, + "mac": null, + "agent_os_type": "NO_HOST", + "image_name": null, + "actor_process_image_name": null, + "actor_process_command_line": null, + "actor_process_image_md5": null, + "actor_process_image_path": null, + "actor_process_os_pid": null, + "actor_process_image_sha256": null, + "actor_process_signature_status": "N/A", + "actor_process_signature_vendor": null, + "actor_thread_thread_id": null, + "fw_is_phishing": "N/A", + "action_local_ip": null, + "action_local_port": null, + "fw_misc": null, + "mitre_tactic_id_and_name": null, + "mitre_technique_id_and_name": null, + "module_id": null, + "fw_vsys": null, + "os_actor_process_command_line": null, + "os_actor_thread_thread_id": null, + "os_actor_process_image_name": null, + "os_actor_process_os_pid": null, + "os_actor_process_image_sha256": null, + "os_actor_process_signature_status": "N/A", + "os_actor_process_signature_vendor": null, + "os_actor_effective_username": null, + "action_process_signature_status": "N/A", + "action_process_signature_vendor": null, + "action_registry_data": null, + "action_registry_full_key": null, + "action_external_hostname": null, + "action_remote_ip": null, + "action_remote_port": null, + "matching_service_rule_id": "16", + "fw_interface_from": null, + "starred": false, + "action_process_image_command_line": null, + "action_process_image_name": null, + "action_process_image_sha256": null, + "fw_url_domain": null, + "user_agent": null, + "fw_xff": null, + "external_id": "0dc55b4f-3e15-4380-9efd-706a67b16c34", + "severity": "medium", + "matching_status": "UNMATCHABLE", + "end_match_attempt_ts": null, + "local_insert_ts": 1708939170883, + "last_modified_ts": null, + "bioc_indicator": null, + "attempt_counter": 0, + "bioc_category_enum_key": null, + "case_id": 1, + "deduplicate_tokens": null, + "filter_rule_id": null, + "agent_version": null, + "agent_ip_addresses_v6": null, + "agent_data_collection_status": null, + "agent_is_vdi": null, + "agent_install_type": "NA", + "agent_host_boot_time": null, + "event_sub_type": null, + "association_strength": null, + "dst_association_strength": null, + "story_id": null, + "event_id": null, + "event_timestamp": null, + "actor_process_instance_id": null, + "actor_process_causality_id": null, + "actor_causality_id": null, + "causality_actor_process_execution_time": null, + "action_registry_key_name": null, + "action_registry_value_name": null, + "action_local_ip_v6": null, + "action_remote_ip_v6": null, + "action_process_instance_id": null, + "action_process_causality_id": null, + "os_actor_process_instance_id": null, + "os_actor_process_image_path": null, + "os_actor_process_causality_id": null, + "os_actor_causality_id": null, + "dst_agent_id": null, + "dst_causality_actor_process_execution_time": null, + "dst_action_external_hostname": null, + "dst_action_country": null, + "dst_action_external_port": null, + "is_pcap": false, + "image_id": null, + "container_name": null, + "namespace": null, + "alert_type": "Unclassified", + "resolution_status": "STATUS_010_NEW", + "resolution_comment": null, + "dynamic_fields": null, + "tags": "DS:Microsoft Graph", + "malicious_urls": null, + "alert_id": "1", + "detection_timestamp": 1708939168584, + "name": "DLP policy (Custom policy) matched for email with subject (Splunk Report: High Or Critical Priority Host With Malware - 15 min) - dlcc6c6fab-0202-be70-c800-08dc36ab8c3e", + "endpoint_id": null, + "description": "EMPTY", + "host_ip": null, + "host_name": null, + "source": "Correlation", + "action": "DETECTED", + "action_pretty": "Detected", + "user_name": null, + "events_length": 1, + "original_tags": "DS:Microsoft Graph", + "host_ip_list": [], + "agentossubtype": null, + "fwappcategory": null, + "fwappid": null, + "fwappsubcategory": null, + "fwapptechnology": null, + "causalityactorprocesscommandline": null, + "causalityactorprocessimagemd5": null, + "causalityactorprocessimagename": null, + "causalityactorprocessimagepath": null, + "causalityactorprocessimagesha256": null, + "causalityactorprocesssignaturestatus": "N/A", + "causalityactorprocesssignaturevendor": null, + "causalityactorcausalityid": null, + "identitysubtype": null, + "identitytype": null, + "operationname": null, + "cloudprovider": null, + "referencedresource": null, + "resourcesubtype": null, + "resourcetype": null, + "clustername": null, + "containerid": null, + "containsfeaturedhost": "NO", + "containsfeaturedip": "NO", + "containsfeatureduser": "NO", + "actioncountry": "UNKNOWN", + "fwinterfaceto": null, + "dnsqueryname": null, + "agentdevicedomain": null, + "fwemailrecipient": null, + "fwemailsender": null, + "fwemailsubject": null, + "eventtype": null, + "iswhitelisted": false, + "actionfilemacrosha256": null, + "actionfilemd5": null, + "actionfilename": null, + "actionfilepath": null, + "actionfilesha256": null, + "fwdevicename": null, + "fwruleid": null, + "fwrule": null, + "fwserialnumber": null, + "agentfqdn": null, + "agentostype": "NO_HOST", + "imagename": null, + "actorprocessimagename": null, + "actorprocesscommandline": null, + "actorprocessimagemd5": null, + "actorprocessimagepath": null, + "actorprocessospid": null, + "actorprocessimagesha256": null, + "actorprocesssignaturestatus": "N/A", + "actorprocesssignaturevendor": null, + "actorthreadthreadid": null, + "fwisphishing": "N/A", + "actionlocalip": null, + "actionlocalport": null, + "fwmisc": null, + "mitretacticidandname": null, + "mitretechniqueidandname": null, + "moduleid": null, + "fwvsys": null, + "osactorprocesscommandline": null, + "osactorthreadthreadid": null, + "osactorprocessimagename": null, + "osactorprocessospid": null, + "osactorprocessimagesha256": null, + "osactorprocesssignaturestatus": "N/A", + "osactorprocesssignaturevendor": null, + "osactoreffectiveusername": null, + "actionprocesssignaturestatus": "N/A", + "actionprocesssignaturevendor": null, + "actionregistrydata": null, + "actionregistryfullkey": null, + "actionexternalhostname": null, + "actionremoteip": null, + "actionremoteport": null, + "matchingserviceruleid": "16", + "fwinterfacefrom": null, + "actionprocessimagecommandline": null, + "actionprocessimagename": null, + "actionprocessimagesha256": null, + "fwurldomain": null, + "useragent": null, + "fwxff": null, + "externalid": "0dc55b4f-3e15-4380-9efd-706a67b16c34", + "matchingstatus": "UNMATCHABLE", + "endmatchattemptts": null, + "localinsertts": 1708939170883, + "lastmodifiedts": null, + "biocindicator": null, + "attemptcounter": 0, + "bioccategoryenumkey": null, + "caseid": 1, + "deduplicatetokens": null, + "filterruleid": null, + "agentversion": null, + "agentipaddressesv6": null, + "agentdatacollectionstatus": null, + "agentisvdi": null, + "agentinstalltype": "NA", + "agenthostboottime": null, + "eventsubtype": null, + "associationstrength": null, + "dstassociationstrength": null, + "storyid": null, + "eventid": null, + "eventtimestamp": null, + "actorprocessinstanceid": null, + "actorprocesscausalityid": null, + "actorcausalityid": null, + "causalityactorprocessexecutiontime": null, + "actionregistrykeyname": null, + "actionregistryvaluename": null, + "actionlocalipv6": null, + "actionremoteipv6": null, + "actionprocessinstanceid": null, + "actionprocesscausalityid": null, + "osactorprocessinstanceid": null, + "osactorprocessimagepath": null, + "osactorprocesscausalityid": null, + "osactorcausalityid": null, + "dstagentid": null, + "dstcausalityactorprocessexecutiontime": null, + "dstactionexternalhostname": null, + "dstactioncountry": null, + "dstactionexternalport": null, + "ispcap": false, + "imageid": null, + "containername": null, + "alerttype": "Unclassified", + "resolutionstatus": "STATUS_010_NEW", + "resolutioncomment": null, + "dynamicfields": null, + "maliciousurls": null, + "alertid": "1", + "detectiontimestamp": 1708939168584, + "endpointid": null, + "hostip": null, + "hostname": null, + "actionpretty": "Detected", + "username": null, + "eventslength": 1, + "originaltags": "DS:Microsoft Graph", + "hostiplist": [] + } + ], + "file_artifacts": [], + "network_artifacts": [], + "id": "1", + "owner": "" +} \ No newline at end of file diff --git a/Packs/CortexXDR/ReleaseNotes/6_1_20.md b/Packs/CortexXDR/ReleaseNotes/6_1_20.md new file mode 100644 index 000000000000..5f714f1481f0 --- /dev/null +++ b/Packs/CortexXDR/ReleaseNotes/6_1_20.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Palo Alto Networks Cortex XDR - Investigation and Response + +Added support for mirrored flexible close-reason mapping from Cortex XSOAR > Cortex XDR and vice-versa. \ No newline at end of file diff --git a/Packs/CortexXDR/pack_metadata.json b/Packs/CortexXDR/pack_metadata.json index d11c6d964ada..4b26817b5ea1 100644 --- a/Packs/CortexXDR/pack_metadata.json +++ b/Packs/CortexXDR/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Cortex XDR by Palo Alto Networks", "description": "Automates Cortex XDR incident response, and includes custom Cortex XDR incident views and layouts to aid analyst investigations.", "support": "xsoar", - "currentVersion": "6.1.19", + "currentVersion": "6.1.20", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", diff --git a/Packs/ctf01/ReleaseNotes/1_0_10.md b/Packs/ctf01/ReleaseNotes/1_0_10.md new file mode 100644 index 000000000000..76c5ad3a2e17 --- /dev/null +++ b/Packs/ctf01/ReleaseNotes/1_0_10.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Cortex XDR - IR CTF + +Added support for mirrored flexible close-reason mapping from Cortex XSOAR > Cortex XDR and vice-versa in `handle_outgoing_issue_closure` in `CoreIRApiModule`. Does not affect this module. \ No newline at end of file diff --git a/Packs/ctf01/pack_metadata.json b/Packs/ctf01/pack_metadata.json index 99ec88b0e37e..c99f15380c8b 100644 --- a/Packs/ctf01/pack_metadata.json +++ b/Packs/ctf01/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Capture The Flag - 01", "description": "XSOAR's Capture the flag (CTF)", "support": "xsoar", - "currentVersion": "1.0.9", + "currentVersion": "1.0.10", "serverMinVersion": "8.2.0", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex",