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())