XDRSyncScript Deprecated
Deprecated. No available replacement. Syncs a single incident between Demisto and XDR. This script always uses the xdr-get-incident-extra-data command and outputs to the context the entire incident JSON. When the incident is updated in XDR, the Demisto incident will be updated accordingly and the default playbook will rerun. When an incident is updated in Demisto, the script will execute the xdr-update-incident command and update the incident in XDR.
python · Cortex XDR by Palo Alto Networks
Source
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 import dateparser from CommonServerUserPython import * from dateutil import parser import copy from typing import Optional, Dict import traceback # XDR_FIELDS INCIDENT_ID_XDR_FIELD = "incident_id" MANUAL_SEVERITY_XDR_FIELD = "manual_severity" MANUAL_DESCRIPTION_XDR_FIELD = "manual_description" ASSIGNED_USER_MAIL_XDR_FIELD = "assigned_user_mail" HIGH_SEVERITY_ALERT_COUNT_XDR_FIELD = "high_severity_alert_count" HOST_COUNT_XDR_FIELD = "host_count" XDR_URL_XDR_FIELD = "xdr_url" ASSIGNED_USER_PRETTY_NAME_XDR_FIELD = "assigned_user_pretty_name" ALERT_COUNT_XDR_FIELD = "alert_count" MED_SEVERITY_ALERT_COUNT_XDR_FIELD = "med_severity_alert_count" USER_COUNT_XDR_FIELD = "user_count" SEVERITY_XDR_FIELD = "severity" LOW_SEVERITY_ALERT_COUNT_XDR_FIELD = "low_severity_alert_count" STATUS_XDR_FIELD = "status" DESCRIPTION_XDR_FIELD = "description" RESOLVE_COMMENT_XDR_FIELD = "resolve_comment" NOTES_XDR_FIELD = "notes" CREATION_TIME_XDR_FIELD = "creation_time" DETECTION_TIME_XDR_FIELD = "detection_time" MODIFICATION_TIME_XDR_FIELD = "modification_time" XDR_INCIDENT_FIELDS = [ INCIDENT_ID_XDR_FIELD, MANUAL_SEVERITY_XDR_FIELD, MANUAL_DESCRIPTION_XDR_FIELD, ASSIGNED_USER_MAIL_XDR_FIELD, HIGH_SEVERITY_ALERT_COUNT_XDR_FIELD, HOST_COUNT_XDR_FIELD, XDR_URL_XDR_FIELD, ASSIGNED_USER_PRETTY_NAME_XDR_FIELD, ALERT_COUNT_XDR_FIELD, MED_SEVERITY_ALERT_COUNT_XDR_FIELD, USER_COUNT_XDR_FIELD, SEVERITY_XDR_FIELD, LOW_SEVERITY_ALERT_COUNT_XDR_FIELD, STATUS_XDR_FIELD, DESCRIPTION_XDR_FIELD, RESOLVE_COMMENT_XDR_FIELD, NOTES_XDR_FIELD, CREATION_TIME_XDR_FIELD, DETECTION_TIME_XDR_FIELD, MODIFICATION_TIME_XDR_FIELD ] SEVERITY_MAPPING = { "low": 1, "medium": 2, "high": 3 } def compare_incident_in_demisto_vs_xdr_context(incident_in_demisto, xdr_incident_in_context, incident_id, fields_mapping): modified_in_demisto = parser.parse(incident_in_demisto.get("modified")).timestamp() * 1000 modified_in_xdr_in_context = int(xdr_incident_in_context.get("modification_time")) incident_in_demisto_was_modified = False xdr_update_args: Dict[str, Optional[str]] = {} if modified_in_demisto > modified_in_xdr_in_context: if ASSIGNED_USER_MAIL_XDR_FIELD in fields_mapping: field_name_in_demisto = fields_mapping[ASSIGNED_USER_MAIL_XDR_FIELD] if field_name_in_demisto == "owner": # if the ASSIGNED USER MAIL mapped to Demisto base field owner assigned_user_mail_current = incident_in_demisto.get("owner") else: assigned_user_mail_current = incident_in_demisto.get("CustomFields").get(field_name_in_demisto) assigned_user_mail_previous = xdr_incident_in_context[ASSIGNED_USER_MAIL_XDR_FIELD] if not assigned_user_mail_current and not assigned_user_mail_previous: # do nothing pass elif not assigned_user_mail_current and assigned_user_mail_previous: incident_in_demisto_was_modified = True xdr_update_args[ASSIGNED_USER_MAIL_XDR_FIELD] = "none" elif assigned_user_mail_current != assigned_user_mail_previous: incident_in_demisto_was_modified = True xdr_update_args[ASSIGNED_USER_MAIL_XDR_FIELD] = assigned_user_mail_current if ASSIGNED_USER_PRETTY_NAME_XDR_FIELD in fields_mapping: field_name_in_demisto = fields_mapping[ASSIGNED_USER_PRETTY_NAME_XDR_FIELD] assigned_user_pretty_name_current = incident_in_demisto.get("CustomFields").get(field_name_in_demisto) assigned_user_pretty_name_previous = xdr_incident_in_context[ASSIGNED_USER_PRETTY_NAME_XDR_FIELD] if assigned_user_pretty_name_current != assigned_user_pretty_name_previous: incident_in_demisto_was_modified = True xdr_update_args[ASSIGNED_USER_PRETTY_NAME_XDR_FIELD] = assigned_user_pretty_name_current if STATUS_XDR_FIELD in fields_mapping: field_name_in_demisto = fields_mapping[STATUS_XDR_FIELD] status_current = incident_in_demisto.get("CustomFields").get(field_name_in_demisto) status_previous = xdr_incident_in_context[STATUS_XDR_FIELD] if status_current != status_previous: incident_in_demisto_was_modified = True xdr_update_args[STATUS_XDR_FIELD] = status_current if SEVERITY_XDR_FIELD in fields_mapping: field_name_in_demisto = fields_mapping[SEVERITY_XDR_FIELD] severity_previous = xdr_incident_in_context[SEVERITY_XDR_FIELD] if field_name_in_demisto == "severity": # if field mapped to original demisto severity field then we should get it directly from incident severity_current = incident_in_demisto.get(field_name_in_demisto) severity_mapping = { 1: "low", 2: "medium", 3: "high", 4: "high" } if severity_current != 0 and severity_current != severity_previous: incident_in_demisto_was_modified = True xdr_update_args[MANUAL_SEVERITY_XDR_FIELD] = severity_mapping[severity_current] else: severity_current = incident_in_demisto.get("CustomFields").get(field_name_in_demisto) if severity_current != severity_previous: incident_in_demisto_was_modified = True if severity_current is None or severity_current == '': xdr_update_args[MANUAL_SEVERITY_XDR_FIELD] = "none" else: xdr_update_args[MANUAL_SEVERITY_XDR_FIELD] = severity_current if RESOLVE_COMMENT_XDR_FIELD in fields_mapping: field_name_in_demisto = fields_mapping[RESOLVE_COMMENT_XDR_FIELD] resolve_comment_current = incident_in_demisto.get("CustomFields").get(field_name_in_demisto) resolve_comment_previous = xdr_incident_in_context[RESOLVE_COMMENT_XDR_FIELD] if resolve_comment_current != resolve_comment_previous: incident_in_demisto_was_modified = True xdr_update_args[RESOLVE_COMMENT_XDR_FIELD] = resolve_comment_current if not xdr_update_args: return False, {} xdr_update_args["incident_id"] = incident_id return incident_in_demisto_was_modified, xdr_update_args def compare_incident_in_xdr_vs_previous_xdr_in_context(latest_incident_in_xdr, xdr_incident_in_context, fields_mapping): # check if incident in xdr was modified demisto_update_args = {} incident_in_xdr_was_modified = False if latest_incident_in_xdr: for field_in_xdr in XDR_INCIDENT_FIELDS: if field_in_xdr in fields_mapping: current_value = latest_incident_in_xdr.get(field_in_xdr) previous_value = xdr_incident_in_context.get(field_in_xdr) if not current_value and not previous_value: # both non existing values - ignore pass if field_in_xdr == 'severity': current_value = SEVERITY_MAPPING[current_value] if current_value != previous_value: incident_in_xdr_was_modified = True field_name_in_demisto = fields_mapping[field_in_xdr] if current_value is None: demisto_update_args[field_name_in_demisto] = "" else: demisto_update_args[field_name_in_demisto] = current_value modification_time = demisto_update_args.get('xdrmodificationtime') if modification_time: demisto_update_args['xdrmodificationtime'] = (datetime.fromtimestamp(int(modification_time) / 1000). isoformat()) + 'Z' return incident_in_xdr_was_modified, demisto_update_args def args_to_str(demisto_args, latest_incident_in_xdr): args_to_str = "" args_copy = copy.deepcopy(demisto_args) if isinstance(latest_incident_in_xdr, dict): args_copy["xdr_incident_from_previous_run"] = json.dumps(latest_incident_in_xdr) args_copy["first"] = "false" for arg_key in args_copy: arg_value = args_copy[arg_key] args_to_str += '{}=`{}` '.format(arg_key, arg_value) return args_to_str def get_latest_incident_from_xdr(incident_id, return_only_updated=False): # Check if the incident was changed in xdr latest_incident_in_xdr_result = demisto.executeCommand("xdr-get-incident-extra-data", {"incident_id": incident_id, "return_only_updated_incident": return_only_updated}) # If the incident wasn't changed then return empty dicts if "The incident was not modified in XDR" in latest_incident_in_xdr_result[0]["Contents"]: return {}, {}, {} elif is_error(latest_incident_in_xdr_result): raise ValueError("Failed to execute xdr-get-incident-extra-data command. Error: {}".format( get_error(latest_incident_in_xdr_result))) latest_incident_in_xdr = latest_incident_in_xdr_result[0]["Contents"].get("incident") # no need to pass the whole incident with extra data - it will be too big json to pass # just the basic incident details if 'alerts' in latest_incident_in_xdr: del latest_incident_in_xdr['alerts'] if 'file_artifacts' in latest_incident_in_xdr: del latest_incident_in_xdr['file_artifacts'] if 'network_artifacts' in latest_incident_in_xdr: del latest_incident_in_xdr['network_artifacts'] latest_incident_in_xdr_markdown = latest_incident_in_xdr_result[0]["HumanReadable"] return latest_incident_in_xdr_result, latest_incident_in_xdr, latest_incident_in_xdr_markdown def create_incident_from_saved_data(demisto_incident, field_mapping, include_extra_data=False): created_incident = {} custom_fields = demisto_incident.get('CustomFields', {}) if include_extra_data: # need to return the incident with extra fields fields_to_create = ['xdralerts', 'xdrfileartifacts', 'xdrnetworkartifacts'] for field in fields_to_create: created_incident[field] = custom_fields.get(field) return created_incident for field in XDR_INCIDENT_FIELDS: field_in_xdr = field_mapping.get(field) if field_in_xdr: if field_in_xdr == 'xdrmodificationtime': modification_time_in_utc_format = custom_fields.get(field_in_xdr) if not modification_time_in_utc_format: modification_time = [label.get('value') for label in demisto_incident.get('labels', []) if label.get('type', '') == 'modification_time'] created_incident[field] = modification_time[0] if modification_time else 0 else: date = dateparser.parse(modification_time_in_utc_format, settings={'TIMEZONE': 'UTC'}) if date is None: # if date is None it means dateparser failed to parse it raise ValueError(f'The modification date of the incident is invalid: ' f'{modification_time_in_utc_format}') created_incident[field] = int(date.timestamp() * 1000) elif field == 'severity': created_incident[field] = demisto_incident.get(field) # severity fields is in the incident root else: created_incident[field] = custom_fields.get(field_in_xdr) created_incident['incident_id'] = custom_fields.get('xdrincidentid') return created_incident def xdr_incident_sync(incident_id, fields_mapping, xdr_incident_from_previous_run, first_run, xdr_alerts_field, xdr_file_artifacts_field, xdr_network_artifacts_field, incident_in_demisto, playbook_to_run, verbose=True): if first_run: xdr_incident_from_previous_run = create_incident_from_saved_data(incident_in_demisto, fields_mapping) else: if xdr_incident_from_previous_run: xdr_incident_from_previous_run = json.loads(xdr_incident_from_previous_run) else: raise ValueError("xdr_incident_from_previous_run expected to contain incident JSON, but passed None") demisto.debug("Check if incident id '{}' was modified in demisto".format(incident_id)) incident_in_demisto_was_modified, xdr_update_args = compare_incident_in_demisto_vs_xdr_context( incident_in_demisto, xdr_incident_from_previous_run, incident_id, fields_mapping) if incident_in_demisto_was_modified: # update xdr and finish the script demisto.debug("the incident in demisto was modified, updating the incident in xdr accordingly. ") demisto.debug("xdr_update_args: {}".format(json.dumps(xdr_update_args, indent=4))) res = demisto.executeCommand("xdr-update-incident", xdr_update_args) if is_error(res): raise ValueError(get_error(res)) latest_incident_in_xdr_result, latest_incident_in_xdr, latest_incident_in_xdr_markdown = \ get_latest_incident_from_xdr(incident_id) xdr_incident = latest_incident_in_xdr_result[0]['Contents'] update_incident = dict() update_incident[xdr_alerts_field] = replace_in_keys(xdr_incident.get('alerts').get('data', []), '_', '') update_incident[xdr_file_artifacts_field] = replace_in_keys(xdr_incident.get('file_artifacts').get('data', []), '_', '') update_incident[xdr_network_artifacts_field] = replace_in_keys(xdr_incident.get('network_artifacts').get('data', []), '_', '') update_incident['lastmirroredintime'] = datetime.now().isoformat() + 'Z' # update the last mirrored-in time if latest_incident_in_xdr.get('severity'): latest_incident_in_xdr['severity'] = SEVERITY_MAPPING[latest_incident_in_xdr.get('severity')] res = demisto.executeCommand("setIncident", update_incident) if is_error(res): raise ValueError(get_error(res)) # update the context with latest incident from xdr demisto.results(latest_incident_in_xdr_result) if verbose: return_outputs( "Incident in Demisto was modified, updating incident in XDR accordingly.\n\n{}".format(xdr_update_args), None) return latest_incident_in_xdr demisto.debug("Check if incident id '{}' was modified in XDR".format(incident_id)) latest_incident_in_xdr_result, latest_incident_in_xdr, _ = get_latest_incident_from_xdr(incident_id, return_only_updated=True) incident_in_xdr_was_modified, demisto_update_args = \ compare_incident_in_xdr_vs_previous_xdr_in_context(latest_incident_in_xdr, xdr_incident_from_previous_run, fields_mapping) if incident_in_xdr_was_modified: demisto.debug("the incident in xdr was modified, updating the incident in demisto") demisto.debug("demisto_update_args: {}".format(json.dumps(demisto_update_args, indent=4))) xdr_incident = latest_incident_in_xdr_result[0]['Contents'] demisto_update_args[xdr_alerts_field] = replace_in_keys(xdr_incident.get('alerts').get('data', []), '_', '') demisto_update_args[xdr_file_artifacts_field] = replace_in_keys( xdr_incident.get('file_artifacts').get('data', []), '_', '') demisto_update_args[xdr_network_artifacts_field] = replace_in_keys( xdr_incident.get('network_artifacts').get('data', []), '_', '') demisto_update_args['lastmirroredintime'] = datetime.now().isoformat() + 'Z' res = demisto.executeCommand("setIncident", demisto_update_args) if is_error(res): raise ValueError(get_error(res)) demisto.results(latest_incident_in_xdr_result) if verbose: return_outputs( "Incident in XDR was modified, updating incident in Demisto accordingly.\n\n{}".format( demisto_update_args), None) if playbook_to_run: demisto.executeCommand("setPlaybook", {"name": playbook_to_run}) else: # rerun the playbook the current playbook demisto.executeCommand("setPlaybook", {}) if latest_incident_in_xdr.get('severity'): latest_incident_in_xdr['severity'] = SEVERITY_MAPPING[latest_incident_in_xdr.get('severity')] return latest_incident_in_xdr if first_run: latest_incident_in_xdr_result = create_incident_from_saved_data(incident_in_demisto, fields_mapping, include_extra_data=True) demisto.results(latest_incident_in_xdr_result) # set the incident markdown field xdr_incident = latest_incident_in_xdr_result update_incident = dict() update_incident[xdr_alerts_field] = replace_in_keys(xdr_incident.get('xdralerts'), '_', '') update_incident[xdr_file_artifacts_field] = replace_in_keys(xdr_incident.get('xdrfileartifacts'), '_', '') update_incident[xdr_network_artifacts_field] = replace_in_keys(xdr_incident.get('xdrnetworkartifacts'), '_', '') demisto.results(update_incident) res = demisto.executeCommand("setIncident", update_incident) if is_error(res): raise ValueError(get_error(res)) if verbose: return_outputs("Nothing to sync.", None) return xdr_incident_from_previous_run def main(args): fields_mapping = {} for xdr_field in XDR_INCIDENT_FIELDS: if xdr_field in args and args.get(xdr_field) is not None: custom_field_in_demisto = args.get(xdr_field) fields_mapping[xdr_field] = custom_field_in_demisto incident_id = args.get('incident_id') first_run = args.get('first') == 'true' interval = int(args.get('interval')) xdr_incident_from_previous_run = args.get('xdr_incident_from_previous_run') xdr_alerts_field = args.get('xdr_alerts') xdr_file_artifacts_field = args.get('xdr_file_artifacts') xdr_network_artifacts_field = args.get('xdr_network_artifacts') verbose = args.get('verbose') == 'true' playbook_to_run = args.get('playbook_to_run') # get current running incident incident_in_demisto = demisto.incident() if not incident_in_demisto: raise ValueError("Error - demisto.incident() expected to return current incident " "from context but returned None") xdr_incident_markdown_field = args.get('xdr_incident_markdown_field') # deprecated if xdr_incident_markdown_field: # deprecated field return_error('Deprecated xdr_incident_markdown_field argument, instead use xdr_alerts, xdr_file_artifacts, ' 'xdr_network_artifacts. For more information follow Demisto documentation.', None) latest_incident_in_xdr = None previous_scheduled_task_id = demisto.get(demisto.context(), 'XDRSyncScriptTaskID') if first_run and previous_scheduled_task_id: # it means someone rerun the playbook, so we stop the previous scheduled task and set the task ID to be empty. if verbose: demisto.debug('Stopping previous scheduled task with ID: {}'.format(previous_scheduled_task_id)) demisto.executeCommand('StopScheduledTask', {'taskID': previous_scheduled_task_id}) demisto.setContext('XDRSyncScriptTaskID', '') try: latest_incident_in_xdr = xdr_incident_sync(incident_id, fields_mapping, xdr_incident_from_previous_run, first_run, xdr_alerts_field, xdr_file_artifacts_field, xdr_network_artifacts_field, incident_in_demisto, playbook_to_run, verbose) except Exception as e: if verbose: raise demisto.error(traceback.format_exc()) return_error(str(e), e) finally: # even if error occurred keep trigger sync if latest_incident_in_xdr is None: args = args_to_str(args, xdr_incident_from_previous_run) else: args = args_to_str(args, latest_incident_in_xdr) res = demisto.executeCommand("ScheduleCommand", { 'command': '''!XDRSyncScript {}'''.format(args), 'cron': '*/{} * * * *'.format(interval), 'times': 1 }) if is_error(res): # return the error entries to warroom demisto.results(res) return scheduled_task_id = res[0]["Contents"].get("id") demisto.setContext("XDRSyncScriptTaskID", scheduled_task_id) if verbose: demisto.results("XDRSyncScriptTaskID: {}".format(scheduled_task_id)) if __name__ == 'builtins': main(demisto.args())