import json import logging import time from datetime import timezone import demistomock as demisto # noqa: F401 import resilient import urllib3 from CommonServerPython import * # noqa: F401 from resilient.co3 import SimpleClient """ IMPORTS """ logging.basicConfig() # disable insecure warnings urllib3.disable_warnings() try: # disable 'warning' logs from 'resilient.co3' logging.getLogger("resilient.co3").setLevel(logging.ERROR) except Exception: # client with no co3 instance should pass this exception pass """ GLOBAL VARS """ DEMISTO_PARAMS = demisto.params() if not DEMISTO_PARAMS["proxy"]: for var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]: if os.environ.get(var): del os.environ[var] URL = DEMISTO_PARAMS["server"][:-1] if DEMISTO_PARAMS["server"].endswith("/") else DEMISTO_PARAMS["server"] # Remove the http/s from the url (It's added automatically later) URL = URL.replace("http://", "").replace("https://", "") # Split the URL into two parts hostname & port SERVER, PORT = URL.rsplit(":", 1) if ":" in URL else (URL, "443") ORG_NAME = DEMISTO_PARAMS["org"] USERNAME = DEMISTO_PARAMS.get("credentials", {}).get("identifier") PASSWORD = DEMISTO_PARAMS.get("credentials", {}).get("password") API_KEY_ID = DEMISTO_PARAMS.get("credentials_api_key", {}).get("identifier") or DEMISTO_PARAMS.get("api_key_id") API_KEY_SECRET = DEMISTO_PARAMS.get("credentials_api_key", {}).get("password") or DEMISTO_PARAMS.get("api_key_secret") USE_SSL = not DEMISTO_PARAMS.get("insecure", False) MAX_FETCH = DEMISTO_PARAMS.get("max_fetch", "1000") TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" UTC = timezone.utc # noqa: UP017 INCIDENT_TYPE_DICT = { "CommunicationError": 17, "DenialOfService": 21, "ImproperDisposal:DigitalAsset": 6, "ImproperDisposal:documents/files": 7, "LostDocuments/files/records": 4, "LostPC/laptop/tablet": 3, "LostPDA/smartphone": 1, "LostStorageDevice/media": 8, "Malware": 19, "NotAnIssue": 23, "Other": 18, "Phishing": 22, "StolenDocuments/files/records": 11, "StolenPC/laptop/tablet": 12, "StolenPDA/Smartphone": 13, "StolenStorageDevice/media": 14, "SystemIntrusion": 20, "TBD/Unknown": 16, "Vendor/3rdPartyError": 15, } NIST_DICT = { "Attrition": 2, "E-mail": 4, "External/RemovableMedia": 1, "Impersonation": 5, "ImproperUsage": 6, "Loss/TheftOfEquipment": 7, "Other": 8, "Web": 3, } NIST_ID_DICT = { 2: "Attrition", 4: "E-mail", 1: "External/RemovableMedia", 5: "Impersonation", 6: "ImproperUsage", 7: "Loss/TheftOfEquipment", 8: "Other", 3: "Web", } SEVERITY_CODE_DICT = {"Low": 4, "Medium": 5, "High": 6} RESOLUTION_DICT = {7: "Unresolved", 8: "Duplicate", 9: "Not an Issue", 10: "Resolved"} RESOLUTION_TO_ID_DICT = {"Unresolved": 7, "Duplicate": 8, "NotAnIssue": 9, "Resolved": 10} MIRROR_STATUS_DICT = { "Unresolved": "Other", "Duplicate": "Duplicate", "Not an Issue": "False Positive", "Resolved": "Resolved", } XSOAR_CLOSE_REASON_MAPPING = {"Other": 7, "Duplicate": 8, "False Positive": 9, "Resolved": 10} EXP_TYPE_ID_DICT = {1: "Unknown", 2: "ExternalParty", 3: "Individual"} OBJECT_ACTION_TYPE_TO_ID = { "Incident": 0, "Task": 1, "Note": 2, "Milestone": 3, "Artifact": 4, "Attachment": 5, "Email Message": 13, } IBM_QRADAR_INCIDENT_FIELDS = { "description": { "xsoar_name": "description", "description": "Description of the incident.", }, "incident_type_ids": {"xsoar_name": "alerttypeid", "description": "The IDs of the incident types."}, "resolution_id": {"xsoar_name": "ibmsecurityqradarsoarresolution", "description": ""}, "resolution_summary": {"xsoar_name": "ibmsecurityqradarsoarresolutionsummary", "description": ""}, "owner_id": { "xsoar_name": "", "description": "The principal ID of the incident owner.", }, "reporter": { "xsoar_name": "ibmsecurityqradarsoarreportername", "description": "Who reported the incident.", }, "severity_code": { "xsoar_name": "severity", "description": "The severity of the incident. 4 = Low, 5 = Medium, 6 = High.", }, "creator.display_name": { "xsoar_name": "displayname", "description": "The display name of the incident creator.", }, } """ CONSTANTS """ FILE_DOWNLOAD_ERROR_MESSAGE = "Download errorDownload error" SCRIPT_ENTITIES = "entities" DEFAULT_RETURN_LEVEL = "full" DEFAULT_RETRIES = 1 IBM_QRADAR_SOAR_INCIDENT_SCHEMA_NAME = "IBM QRadar SOAR Incident Schema" DEFAULT_SEVERITY_CODE = 5 DEFAULT_TAG_FROM_IBM = "FROM IBM" DEFAULT_TAG_TO_IBM = "FROM XSOAR" """ ENDPOINTS """ SEARCH_INCIDENTS_ENDPOINT = "/incidents/query_paged" """ HELPER FUNCTIONS """ def validate_iso_time_format(iso_time: str) -> str: """ Ensures the input time string does not contain the milliseconds part and the time string ends with a 'Z' to denote Zulu time (UTC). Args: iso_time (str): Time in ISO format to check and modify if needed. Returns: str: The modified iso_time string with a 'Z' suffix if it wasn't already present. """ if not iso_time: return iso_time # Remove milliseconds from the time string. iso_time = iso_time.split(".")[0] if not iso_time.endswith("Z"): iso_time += "Z" return iso_time def normalize_timestamp(timestamp_ms: int | None): """ Converts a timestamp in milliseconds to an ISO 8601 formatted date string in UTC. Parameters: - timestamp_ms (int or float): The timestamp in milliseconds since the Unix epoch. Returns: - str: The ISO 8601 formatted date string (e.g., "2020-08-09T10:00:00Z"). """ if not timestamp_ms: return "" try: # Convert milliseconds to seconds timestamp_s = timestamp_ms / 1000.0 # Create a datetime object in UTC dt = datetime.fromtimestamp(timestamp_s, tz=UTC) # Format the datetime without microseconds and append 'Z' iso_str = dt.strftime("%Y-%m-%dT%H:%M:%SZ") return iso_str except (OverflowError, OSError) as e: raise ValueError("The timestamp is out of the valid range.") from e def prettify_incidents(client, incidents): users = get_users(client) phases = get_phases(client) for incident in incidents: incident["id"] = str(incident["id"]) if isinstance(incident["description"], str): incident["description"] = incident["description"] incident["discovered_date"] = normalize_timestamp(incident["discovered_date"]) incident["created_date"] = normalize_timestamp(incident["create_date"]) incident.pop("create_date", None) incident.pop("inc_training", None) for user in users: if incident["owner_id"] == user["id"]: incident["owner"] = user["fname"] + " " + user["lname"] incident.pop("owner_id", None) break for phase in phases: if incident["phase_id"] == phase["id"]: incident["phase"] = phase["name"] incident.pop("phase_id", None) break incident["severity"] = incident.get("severity_code", DEFAULT_SEVERITY_CODE) if start_date := incident.get("start_date"): incident["date_occurred"] = normalize_timestamp(start_date) incident.pop("start_date", None) if due_date := incident.get("due_date"): incident["due_date"] = normalize_timestamp(due_date) if negative_pr := incident.get("negative_pr_likely"): incident["negative_pr"] = negative_pr incident.pop("negative_pr_likely", None) if exposure_type_id := incident.get("exposure_type_id"): incident["exposure_type"] = EXP_TYPE_ID_DICT.get(exposure_type_id, exposure_type_id) incident.pop("exposure_type_id", None) if nist_attack_vectors := incident.get("nist_attack_vectors"): translated_nist = [] for vector in nist_attack_vectors: translated_nist.append(NIST_ID_DICT[vector]) incident["nist_attack_vectors"] = translated_nist if plan_status := incident.get("plan_status"): incident["plan_status"] = "Active" if plan_status == "A" else "Closed" return incidents def prettify_incident_notes(notes: list[dict]) -> list[dict]: """ Reformatting retrieved incident notes to be more readable. """ formatted_notes = [] notes_copy = notes.copy() while notes_copy: note = notes_copy.pop() demisto.debug(f"prettify_incident_notes {note=}") if note.get("text"): create_date: int | None = note.get("create_date") new_note_obj = { "id": note.get("id", ""), "text": note.get("text"), "created_by": f"{note.get('user_fname', '')} {note.get('user_lname', '')}", "create_date": normalize_timestamp(create_date), "modify_date": note.get("modify_date"), } formatted_notes.append(new_note_obj) return formatted_notes def prettify_incident_tasks(client: SimpleClient, tasks: list[dict]) -> list[dict]: """ Formats and enriches tasks to a more readable data. """ def format_task(task): task.update( { "Phase": get_phase_name(client, task["phase_id"]), "ID": task["id"], "Name": task["name"], "Description": task["description"], "DueDate": normalize_timestamp(task["due_date"]) if task["due_date"] else "No due date", "Status": "Open" if task["status"] == "O" else "Closed", "Required": task["required"], "Owner": f"{task.get('owner_fname', '')} {task.get('owner_lname', '')}", "Creator": "", "Instructions": "", } ) if creator := task.get("creator_principal"): task["Creator"] = creator.get("display_name", "") if instructions := task.get("instructions"): task["Instructions"] = instructions.get("content", "") return task formatted_tasks = [format_task(task) for task in tasks] demisto.debug(f"prettify_incident_tasks {formatted_tasks=}") return formatted_tasks def prepare_search_query_data(args: dict) -> dict: """ Preparing the search query filters and pagination parameters for the `search_incidents` request. """ demisto.debug(f"prepare_search_query_data {args=}") conditions = [] # type: Any if "severity" in args: value = [] severity = args["severity"].split(",") if "Low" in severity: value.append(50) if "Medium" in severity: value.append(51) if "High" in severity: value.append(52) if not value: raise Exception("Severity should be given in capital case and comma separated, e.g. Low,Medium,High") conditions.append({"field_name": "severity_code", "method": "in", "value": value}) if "date-created-before" in args: value = to_timestamp(args["date-created-before"]) conditions.append({"field_name": "create_date", "method": "lte", "value": value}) elif "date-created-after" in args: value = to_timestamp(args["date-created-after"]) conditions.append({"field_name": "create_date", "method": "gte", "value": value}) elif "date-created-within-the-last" in args: if "timeframe" not in args: raise Exception("Timeframe was not given.") within_the_last = int(args["date-created-within-the-last"]) now = int(time.time()) timeframe = args["timeframe"] if timeframe == "days": from_time = now - (60 * 60 * 24 * within_the_last) elif timeframe == "hours": from_time = now - (60 * 60 * within_the_last) else: # timeframe == 'minutes': demisto.debug(f"{timeframe=} should be minutes.") from_time = now - (60 * within_the_last) conditions.extend( ( {"field_name": "create_date", "method": "lte", "value": now * 1000}, {"field_name": "create_date", "method": "gte", "value": from_time * 1000}, ) ) if "date-occurred-before" in args: value = to_timestamp(args["date-occurred-before"]) conditions.append({"field_name": "start_date", "method": "lte", "value": value}) elif "date-occurred-after" in args: value = to_timestamp(args["date-occurred-after"]) conditions.append({"field_name": "start_date", "method": "gte", "value": value}) elif "date-occurred-within-the-last" in args: if "timeframe" not in args: raise Exception("Timeframe was not given.") within_the_last = int(args["date-occurred-within-the-last"]) now = int(time.time()) timeframe = args["timeframe"] if timeframe == "days": from_time = now - (60 * 60 * 24 * within_the_last) elif timeframe == "hours": from_time = now - (60 * 60 * within_the_last) else: # timeframe == 'minutes': demisto.debug(f"{timeframe=} should be minutes.") from_time = now - (60 * within_the_last) conditions.extend( ( {"field_name": "start_date", "method": "lte", "value": now * 1000}, {"field_name": "start_date", "method": "gte", "value": from_time * 1000}, ) ) if "incident-type" in args: type_id = INCIDENT_TYPE_DICT[args["incident-type"]] conditions.append({"field_name": "incident_type_ids", "method": "contains", "value": [type_id]}) if "nist" in args: nist = NIST_DICT[args["nist"]] conditions.append({"field_name": "nist_attack_vectors", "method": "contains", "value": [nist]}) if "status" in args: status = "A" if args["status"] == "Active" else "C" conditions.append({"field_name": "plan_status", "method": "in", "value": [status]}) if "due-in" in args: if "timeframe" not in args: raise Exception("Timeframe was not given.") within_the_last = int(args["due-in"]) now = int(time.time()) timeframe = args["timeframe"] if timeframe == "days": to_time = now + (60 * 60 * 24 * within_the_last) elif timeframe == "hours": to_time = now + (60 * 60 * within_the_last) else: # timeframe == 'minutes': demisto.debug(f"{timeframe=} should be minutes.") to_time = now + (60 * within_the_last) conditions.extend( ( {"field_name": "due_date", "method": "lte", "value": to_time * 1000}, {"field_name": "due_date", "method": "gte", "value": now * 1000}, ) ) if "last-modified-after" in args: value = to_timestamp(args["last-modified-after"]) conditions.append({"field_name": "inc_last_modified_date", "method": "gte", "value": value}) data: Dict[str, Any] = {"filters": [{"conditions": conditions}], "sorts": [{"field_name": "create_date", "type": "asc"}]} # Pagination mechanism. page = int(args.get("page", 0)) page_size = int(args.get("page_size", 0)) limit = int(args.get("limit", MAX_FETCH)) data["length"] = limit # 'limit' parameter is redundant in case proper 'page' and 'page_size' were provided. if page_size > 0 and page > 0: data["start"] = page_size * (page - 1) data["length"] = page_size elif page < 0 or page_size < 0: raise DemistoException("Invalid page number or page size. Page number and page sizes must be positive integers.") demisto.debug(f"prepare_search_query_data {data=}") return data def get_mirroring_data() -> dict: """ Get the integration instance's mirroring configuration parameters. Returns: dict: A dictionary containing the mirroring configuration parameters. """ params = demisto.params() mirror_direction = params.get("mirror_direction") demisto.debug(f"get_mirroring_data {mirror_direction=} | {params=} ") mirror_tags = [params.get("tag_from_ibm"), params.get("tag_to_ibm")] return { "mirror_direction": mirror_direction, "mirror_instance": demisto.integrationInstance(), "mirror_tags": mirror_tags, } def process_raw_incident(client: SimpleClient, incident: dict) -> dict: """ Process a raw incident dictionary by fetching associated artifacts and attachments, removing HTML div tags from the description and normalizing timestamps. Args: client (SimpleClient): The client instance to use for API calls. incident (dict): The raw incident dictionary to process. Returns: dict: The processed incident dictionary. """ incident_id = str(incident.get("id")) demisto.debug(f"process_raw_incident {incident_id=}") if isinstance(incident.get("description"), str): incident["description"] = incident["description"] elif isinstance(incident.get("description"), dict): incident["description"] = incident["description"]["content"] incident["discovered_date"] = normalize_timestamp(incident.get("discovered_date")) incident["create_date"] = normalize_timestamp(incident.get("create_date")) if DEMISTO_PARAMS.get("fetch_notes"): notes = get_incident_notes(client, incident_id) incident["notes"] = prettify_incident_notes(notes) demisto.debug(f"process_raw_incident {[note['text'] for note in incident['notes']]=}") if DEMISTO_PARAMS.get("fetch_tasks"): tasks = get_tasks(client, incident_id) incident["tasks"] = prettify_incident_tasks(client, tasks) attachments_metadata = incident_attachments(client, incident_id) incident["attachments"] = [ { "ID": attachment.get("id"), "Name": attachment.get("name"), "Create Time": attachment.get("created"), # Timestamp in milliseconds. "Size": attachment.get("size"), } for attachment in attachments_metadata ] demisto.debug(f'process_raw_incident {incident["attachments"]=}') artifacts = incident_artifacts(client, incident_id) incident["artifacts"] = [ { "ID": artifact.get("id"), "Type": get_artifact_type(client, artifact.get("type")), "Value": artifact.get("value"), # Timestamp in milliseconds. } for artifact in artifacts ] incident["phase"] = get_phase_name(client, incident["phase_id"]) incident.update(get_mirroring_data()) demisto.debug(f"process_raw_incident processed_incident={incident}") return incident def resolve_field_value(field: str, raw_value: Any) -> dict: """ Resolve an incident's field value for an API PATCH request. """ demisto.debug(f"resolve_field_value {field=} | {type(raw_value)=} | {raw_value=}") # Null values & object-formatted values are returned as-is under 'textarea' key. if not raw_value or isinstance(raw_value, dict): return {"textarea": raw_value or None} elif field in ["severity_code", "owner_id", "resolution_id"]: return {"id": int(raw_value)} elif field in ["reporter", "plan_status", "name"]: return {"text": raw_value} elif field in ["resolution_summary", "description"]: return {"textarea": {"format": "html", "content": raw_value}} elif field in ["incident_type_ids", "nist_attack_vectors"]: return {"ids": raw_value} raise DemistoException(f"Could no resolve field value for field: {field}") def get_field_changes_entry(field: str, old_value: Any, new_value: Any) -> dict: """ Get the field changes entry for an incident update. """ field_changes = { "field": field, "old_value": resolve_field_value(field, old_value), "new_value": resolve_field_value(field, new_value), } return field_changes def prepare_incident_update_dto_for_mirror(client: SimpleClient, incident_id: str, delta: dict) -> dict: """ Prepare an incident update DTO for mirroring data. Args: client (SimpleClient): The client object to interact with the API. incident_id (str): The ID of the incident to be updated. delta (dict): A dictionary containing the fields and their new values to be updated. """ incident = get_incident(client, incident_id) demisto.debug(f"prepare_incident_update_dto_for_mirror {delta=} | {incident=}") changes = [] for field, new_value in delta.items(): # `resolution_id` is updated once the incident is closed or re-opened and requires additional treatment. if field == "resolution_id" and DEMISTO_PARAMS.get("close_ibm_incident"): remote_status = incident["plan_status"] # Handling remote incident reopening. if new_value == "" and remote_status == "C": changes.append(get_field_changes_entry("plan_status", remote_status, "A")) # Remote incident closure handling. else: changes.append(get_field_changes_entry("plan_status", remote_status, "C")) elif field == "ibmsecurityqradarsoarname": # Excluding this field as the 'name' field is also used and read by XSOAR. field = "name" changes.append(get_field_changes_entry(field=field, old_value=incident[field], new_value=new_value)) dto = {"changes": changes} demisto.debug(f"prepare_incident_update_dto_for_mirror {dto=}") return dto def to_timestamp(time_input): if isinstance(time_input, int): # Input is already a timestamp in milliseconds return time_input elif isinstance(time_input, str): # Try to parse the string as an integer timestamp try: timestamp_ms = int(time_input) return timestamp_ms except ValueError: # Not an integer, try to parse as ISO time string try: dt = datetime.strptime(time_input, "%Y-%m-%dT%H:%M:%SZ") dt = dt.replace(tzinfo=UTC) timestamp_ms = int(dt.timestamp() * 1000) return timestamp_ms except ValueError: raise ValueError( f"Invalid time input: '{time_input}' is neither a valid integer timestamp nor a valid ISO time string." ) else: raise TypeError(f"Invalid type for time_input: expected str or int, got {type(time_input).__name__}.") def extract_data_form_other_fields_argument(other_fields, incident, changes): """Extracts the values from other-field argument and build a json object in ibm format to update an incident. Args: other_fields (str): Contains the field that should be changed and the new value ({"name": {"text": "The new name"}}). incident (dict): Contains the old value of the field that should be changed ({"name": "The old name"}). changes (list): Contains the fields that should be changed with the old and new values in IBM format ([{'field': {'name': 'confirmed'}, 'old_value': {'boolean': 'false'}, 'new_value': {'boolean': 'true'}, {'field': {'name': 'name'}, 'old_value': {'text': 'The old name'}, 'new_value': {'text': 'The new name'}}]). """ try: other_fields_json = json.loads(other_fields) except Exception as e: raise Exception("The other_fields argument is not a valid json. " + str(e)) for field_path, field_value in other_fields_json.items(): field_split = field_path.split(".") old_value = dict_safe_get(dict_object=incident, keys=field_split, default_return_value="Not found") if old_value == "Not found": raise Exception("The other_fields argument is invalid. Check the name of the field whether it is the right path") changes.append( { "field": {"name": field_split[-1]}, # The format should be {type: value}. # Because the type is not returned from the API we take the type from the new value. "old_value": {list(field_value.keys())[0]: old_value}, "new_value": field_value, } ) def get_attachment(client: SimpleClient, incident_id: str, attachment_id: str) -> tuple[str, str]: """ Retrieves the name and the contents of an incident's attachment with ID `attachment_id`. """ response = client.get(f"/incidents/{incident_id}/attachments/{attachment_id}") demisto.debug(f"get_attachment {response}") if isinstance(response, dict) and "name" in response: attachment_name = response["name"] else: raise DemistoException(f"Could not retrieve a file with ID {attachment_id}") response: requests.Response = client.get( f"/incidents/{incident_id}/attachments/{attachment_id}/contents", get_response_object=True ) contents = str(response.content) demisto.debug(f"get_attachment {contents}") if FILE_DOWNLOAD_ERROR_MESSAGE in contents: raise DemistoException(f"Could not retrieve a file with ID {attachment_id}") return attachment_name, contents def get_users(client): response = client.get("/users") return response def get_phase_name(client: SimpleClient, phase_id: str) -> str: response = client.get(f"/phases/{phase_id}") return response.get("name") def get_phases(client: SimpleClient): response = client.get("/phases") return response.get("entities", []) def get_tasks(client: SimpleClient, incident_id: str): response = client.get(f"/incidents/{incident_id}/tasks?text_content_output_format=objects_convert_text") return response def update_task(client: SimpleClient, task_id: str, task_dto: dict): """ Updating a remote task with ID `task_id` according to the updated values in `task_dto`. """ response = client.put(f"/tasks/{task_id}", payload=task_dto) return response def search_incidents(client: SimpleClient, args: dict) -> list | dict: """ Search and get IBM QRadar incidents according to filters and pagination parameters. :return: List of IBM QRadar incidents matching the search query. """ search_query_data = prepare_search_query_data(args) return_level = args.get("return_level", DEFAULT_RETURN_LEVEL) endpoint = f"{SEARCH_INCIDENTS_ENDPOINT}?text_content_output_format=objects_convert_text&return_level={return_level}" response = client.post(endpoint, search_query_data) demisto.debug(f"search_incidents {response}") return response["data"] def update_incident(client, incident_id, data): response = client.patch("/incidents/" + str(incident_id), data) return response def get_incident(client: SimpleClient, incident_id, content_format=False): url = "/incidents/" + str(incident_id) if content_format: url += "?text_content_output_format=objects_convert_text" response = client.get(url) return response def list_open_incidents(client): response = client.get("/incidents/open") return response def handle_incoming_incident_resolution(incident_id: str, resolution_id: int, resolution_summary: str) -> dict: """ Resolves XSOAR close reason and creates a closing entry to be posted in the incident's War Room. """ resolution_status = RESOLUTION_DICT.get(resolution_id, "Resolved") demisto.debug(f"handle_incoming_incident_resolution {incident_id=} | {resolution_status=} | {resolution_summary=}") closing_entry = { "Type": EntryType.NOTE, "Contents": { "dbotIncidentClose": True, "closeReason": MIRROR_STATUS_DICT.get(resolution_status, "Resolved"), "closeNotes": f"{resolution_summary}\nClosed on IBM QRadar SOAR".strip(), }, "ContentsFormat": EntryFormat.JSON, } return closing_entry def handle_incoming_incident_reopening(incident_id: str) -> dict: """ Post a reopening entry to the incident's War Room. """ demisto.debug(f"handle_incident_reopening {incident_id=}") reopening_entry = { "Type": EntryType.NOTE, "Contents": {"dbotIncidentReopen": True}, "ContentsFormat": EntryFormat.JSON, } return reopening_entry """ COMMAND FUNCTIONS """ def get_incident_command(client, incident_id): incident = get_incident(client, incident_id) wanted_keys = [ "create_date", "discovered_date", "description", "due_date", "id", "name", "owner_id", "phase_id", "severity_code", "confirmed", "employee_involved", "negative_pr_likely", "confirmed", "start_date", "due_date", "negative_pr_likely", "reporter", "exposure_type_id", "nist_attack_vectors", ] pretty_incident = {k: incident[k] for k in wanted_keys if k in incident} if incident["resolution_id"]: pretty_incident["resolution"] = RESOLUTION_DICT.get(incident["resolution_id"], incident["resolution_id"]) if incident["resolution_summary"]: pretty_incident["resolution_summary"] = incident["resolution_summary"].replace("
", "").replace("
", "") pretty_incident = prettify_incidents(client, [pretty_incident]) result_incident = createContext(pretty_incident, id=None, keyTransform=underscoreToCamelCase, removeNull=True) ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": result_incident} hr_incident = result_incident[:] if hr_incident[0].get("NistAttackVectors"): nist_vectors_str = "" for vector in hr_incident[0].get("NistAttackVectors", []): nist_vectors_str += vector + "\n" hr_incident[0]["NistAttackVectors"] = nist_vectors_str title = "IBM QRadar SOAR incident ID " + str(incident_id) entry = { "Type": entryTypes["note"], "Contents": incident, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown( title, hr_incident, headers=[ "Id", "Name", "Description", "NistAttackVectors", "Phase", "Resolution", "ResolutionSummary", "Owner", "CreatedDate", "DateOccurred", "DiscoveredDate", "DueDate", "NegativePr", "Confirmed", "ExposureType", "Severity", "Reporter", ], ), "EntryContext": ec, } return entry def search_incidents_command(client, args): incidents = search_incidents(client, args) if incidents: pretty_incidents = prettify_incidents(client, incidents) result_incidents = createContext( pretty_incidents, id=None, keyTransform=underscoreToCamelCase, removeNull=True ) # pragma: no cover ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": result_incidents} title = "QRadar SOAR Incidents" entry = { "Type": entryTypes["note"], "Contents": incidents, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown( title, result_incidents, headers=["Id", "Name", "PlanStatus", "CreatedDate", "DiscoveredDate", "Owner", "Phase"], removeNull=True, ), "EntryContext": ec, } return entry else: return "No results found." def update_incident_command(client, args): if len(args) == 1: raise DemistoException("No fields to update were given.") incident_id = args["incident-id"] incident = get_incident(client, incident_id, True) changes = [] if "severity" in args: old_value = incident["severity_code"] severity = args["severity"] new_value = SEVERITY_CODE_DICT.get(severity) changes.append(get_field_changes_entry("severity_code", old_value, new_value)) if "owner" in args: users = get_users(client) old_value = incident["owner_id"] full_name = args["owner"].split(" ") first_name, last_name = full_name[0], full_name[1] new_value = -1 for user in users: if first_name == user["fname"] and last_name == user["lname"]: new_value = user["id"] break if new_value == -1: raise DemistoException("User was not found") changes.append(get_field_changes_entry("owner_id", old_value, new_value)) if "incident-type" in args: old_value = incident["incident_type_ids"] type_id = INCIDENT_TYPE_DICT[args["incident-type"]] new_value_list = old_value[:] new_value_list.append(type_id) changes.append(get_field_changes_entry("incident_type_ids", old_value, new_value_list)) if "nist" in args: old_value = incident["nist_attack_vectors"] nist_id = NIST_DICT[args["nist"]] new_value_list = old_value[:] new_value_list.append(nist_id) changes.append(get_field_changes_entry("nist_attack_vectors", old_value, new_value_list)) if "resolution" in args: old_value = incident["resolution_id"] new_value = RESOLUTION_TO_ID_DICT[args["resolution"]] changes.append(get_field_changes_entry("resolution_id", old_value, new_value)) if "resolution-summary" in args: old_summary = incident["resolution_summary"] new_summary = args["resolution-summary"] changes.append(get_field_changes_entry("resolution_summary", old_summary, new_summary)) if "description" in args: old_description = incident["description"] new_description = args["description"] changes.append(get_field_changes_entry("description", old_description, new_description)) if "name" in args: old_name = incident["name"] new_name = args["name"] changes.append(get_field_changes_entry("name", old_name, new_name)) if other_fields := args.get("other-fields"): extract_data_form_other_fields_argument(other_fields, incident, changes) update_dto = {"changes": changes} demisto.debug(f"update_incident_command: {json.dumps(update_dto, indent=4)}") response = update_incident(client, incident_id, update_dto) demisto.debug(f"update_incident_command {response=!s}") if response.status_code == 200: return f"Incident {incident_id} was updated successfully." else: # pragma: no cover return f"Failed to update incident {incident_id}" def get_members_command(client, incident_id): response = get_members(client, incident_id)["members"] incident = get_incident(client, incident_id) response.append(incident["owner_id"]) users = get_users(client) members = [] for user in users: if user["id"] in response: members.append({"FirstName": user["fname"], "LastName": user["lname"], "ID": user["id"], "Email": user["email"]}) ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": {"Id": incident_id, "Members": members}} title = "Members of incident " + incident_id entry = { "Type": entryTypes["note"], "Contents": members, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, members, ["ID", "LastName", "FirstName", "Email"]), "EntryContext": ec, } return entry def get_members(client, incident_id): response = client.get("/incidents/" + incident_id + "/members") return response def get_users_command(client): response = get_users(client) users = [] for user in response: users.append({"FirstName": user["fname"], "LastName": user["lname"], "ID": user["id"], "Email": user["email"]}) title = "IBM QRadar SOAR Users" entry = { "Type": entryTypes["note"], "Contents": users, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, users, ["ID", "LastName", "FirstName", "Email"]), } return entry def get_tasks_command(client, incident_id): tasks = get_tasks(client, incident_id) tasks = prettify_incident_tasks(client, tasks) for task in tasks: incident_name = task.get("IncidentName", "") ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": {"Id": incident_id, "Name": incident_name, "Tasks": tasks}} title = "Incident " + incident_id + " tasks" entry = { "Type": entryTypes["note"], "Contents": tasks, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown( title, tasks, ["ID", "Name", "Category", "Form", "Status", "DueDate", "Instructions", "UserNotes", "Required", "Creator"], ), "EntryContext": ec, } return entry return "No tasks found for this incident." def update_task_command(client: SimpleClient, args: dict) -> CommandResults: task_id = args.get("task_id") if not task_id: raise DemistoException("task_id is required") task_dto = {} if task_name := args.get("name"): task_dto["name"] = task_name if owner_id := args.get("owner_id"): task_dto["inc_owner_id"] = int(owner_id) if due_date := args.get("due_date"): task_dto["due_date"] = to_timestamp(due_date) if phase := args.get("phase"): task_dto["phase_id"] = phase if instructions := args.get("instructions"): task_dto["instructions"] = instructions if args.get("status") == "Open": task_dto["status"] = "O" elif args.get("status") == "Completed": task_dto["status"] = "C" demisto.debug(f"update_task_command {task_dto=}") update_task(client, task_id, task_dto) return CommandResults(readable_output=f"Task {task_id} updated successfully.") def set_member_command(client, incident_id, members): members = [int(x) for x in members.split(",")] incident = get_incident(client, incident_id) incident_version = incident["vers"] data = {"vers": incident_version, "members": members} response = set_member(client, incident_id, data) users = get_users(client) entry = {} if response: for user in users: if user["id"] in members: if isinstance(response, dict): response.update( {"FirstName": user["fname"], "LastName": user["lname"], "ID": user["id"], "Email": user["email"]} ) else: response.append( {"FirstName": user["fname"], "LastName": user["lname"], "ID": user["id"], "Email": user["email"]} ) ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": {"Id": incident_id, "Members": response}} title = "Members of incident " + incident_id entry = { "Type": entryTypes["note"], "Contents": response, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, response), "EntryContext": ec, } return entry def set_member(client, incident_id, data): response = client.put(f"/incidents/{incident_id}/members", payload=data) return response def close_incident_command(client, incident_id): incident = get_incident(client, incident_id) if not incident["resolution_id"] or not incident["resolution_summary"]: return "Resolution and resolution summary of the incident should be updated before closing an incident." response = close_incident(client, incident_id, incident) if response.status_code == 200: return "Incident " + incident_id + " was closed." return None def close_incident(client, incident_id, incident): old_status = incident["plan_status"] data = {"changes": [get_field_changes_entry("plan_status", old_status, "C")]} return update_incident(client, incident_id, data) def create_incident_command(client, args): incident_name = args["name"] data = {"name": incident_name, "discovered_date": 0} response = create_incident(client, data) hr = {"ID": response["id"], "Name": incident_name} ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": {"Id": str(response["id"]), "Name": incident_name}} title = "Incident " + incident_name + " was created" entry = { "Type": entryTypes["note"], "Contents": response, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, hr), "EntryContext": ec, } return entry def create_incident(client, data): response = client.post("/incidents", data) return response def incident_artifacts_command(client, incident_id): response = incident_artifacts(client, incident_id) if response: users = get_users(client) ec_artifacts = [] hr_artifacts = [] for artifact in response: incident_name = artifact["inc_name"] artifact_object = {} if artifact["description"]: artifact_object["Description"] = artifact["description"] hr_artifact = dict(artifact_object) if artifact["attachment"]: artifact_object["Attachments"] = {} attachment_string = "" artifact_object["Attachments"]["ID"] = artifact["attachment"]["id"] attachment_string += "ID: " + str(artifact_object["Attachments"]["ID"]) + "\n" artifact_object["Attachments"]["Name"] = artifact["attachment"]["name"] attachment_string += "Name: " + artifact_object["Attachments"]["Name"] + "\n" artifact_object["Attachments"]["CreatedDate"] = normalize_timestamp(artifact["attachment"]["created"]) attachment_string += "Created Date: " + artifact_object["Attachments"]["CreatedDate"] + "\n" artifact_object["Attachments"]["ContentType"] = artifact["attachment"]["content_type"] attachment_string += "Content Type : " + artifact_object["Attachments"]["ContentType"] + "\n" artifact_object["Attachments"]["Size"] = artifact["attachment"]["size"] attachment_string += "Size: " + str(artifact_object["Attachments"]["Size"]) + "\n" creator_id = artifact["attachment"]["creator_id"] for user in users: if creator_id == user["id"]: artifact_object["Attachments"]["Creator"] = user["fname"] + " " + user["lname"] attachment_string += "Creator: " + artifact_object["Attachments"]["Creator"] break hr_artifact["Attachments"] = attachment_string hr_artifacts.append(hr_artifact) ec_artifacts.append(artifact_object) ec = { "Resilient.Incidents(val.Id && val.Id === obj.Id)": { "Id": incident_id, "Name": incident_name, "Artifacts": ec_artifacts, } } title = "Incident " + incident_id + " artifacts" entry = { "Type": entryTypes["note"], "Contents": response, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown( title, hr_artifacts, headers=["ID", "Value", "Description", "CreatedDate", "Creator"] ), "EntryContext": ec, } return entry else: return "No artifacts found." def incident_artifacts(client, incident_id): response = client.get("/incidents/" + incident_id + "/artifacts") return response def get_artifact_type(client, artifact_id): response = client.get("/artifact_types/" + str(artifact_id)) return response["name"] def incident_attachments_command(client, incident_id): incident_owner = "" response = incident_attachments(client, incident_id) if response: attachments = [] users = get_users(client) for attachment in response: incident_name = attachment["inc_name"] attachment_object = {} attachment_object["ID"] = attachment["id"] attachment_object["Name"] = attachment["name"] attachment_object["CreatedDate"] = normalize_timestamp(attachment["created"]) attachment_object["Size"] = attachment["size"] attachment_object["ContentType"] = attachment["content_type"] attachment_object["Name"] = attachment["name"] for user in users: if attachment["creator_id"] == user["id"]: attachment_object["Creator"] = user["fname"] + " " + user["lname"] if attachment["inc_owner"] == user["id"]: incident_owner = user["fname"] + " " + user["lname"] attachments.append(attachment_object) ec = { "Resilient.Incidents(val.Id && val.Id === obj.Id)": { "Id": incident_id, "Name": incident_name, "Owner": incident_owner, "Attachments": attachments, } } title = "Incident " + incident_id + " attachments" entry = { "Type": entryTypes["note"], "Contents": response, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, attachments), "EntryContext": ec, } return entry else: return "No attachments found." def incident_attachments(client, incident_id): response = client.get("/incidents/" + incident_id + "/attachments") return response def upload_incident_attachment(client: SimpleClient, incident_id: str, entry_id: str, tag_to_ibm: str): """ Uploads a file from XSOAR to the IBM QRadar SOAR incident with ID `incident_id`. """ try: file_path_obj = demisto.getFilePath(entry_id) except ValueError: raise DemistoException(f"Could not find a file with entry ID: {entry_id}") file_path, file_name = file_path_obj.get("path"), file_path_obj.get("name") # Split the file name into root and extension root, extension = os.path.splitext(file_name) tagged_file_name = f"{root}_{tag_to_ibm}" if extension: tagged_file_name = tagged_file_name + extension response = client.post_attachment( uri=f"/incidents/{incident_id}/attachments", filepath=file_path, filename=tagged_file_name, ) demisto.debug(f"upload_incident_attachment_command {response=}") def get_incident_notes(client: SimpleClient, incident_id: str) -> list: response = client.get(f"/incidents/{incident_id}/comments?text_content_output_format=objects_convert_text") return response def related_incidents_command(client, incident_id): response = related_incidents(client, incident_id)["incidents"] if response: ec_incidents = [] hr_incidents = [] for incident in response: incident_object = { "ID": incident["id"], "Name": incident["name"], "Status": "Active" if incident["plan_status"] == "A" else "Closed", "CreatedDate": normalize_timestamp(incident["create_date"]), } hr_incident = dict(incident_object) if incident["artifacts"]: hr_incident["Artifacts"] = "" artifacts = [] for artifact in incident["artifacts"]: artifact_object = {} artifact_string = "" artifact_object["ID"] = artifact["id"] artifact_string += "ID: " + str(artifact_object["ID"]) + "\n" artifact_object["CreatedDate"] = normalize_timestamp(artifact["created"]) artifact_string += "Created Date: " + artifact_object["CreatedDate"] + "\n" if artifact["description"]: artifact_object["Description"] = artifact["description"] artifact_string += "Description: " + artifact_object["Description"] + "\n" artifact_object["Creator"] = artifact["creator"]["fname"] + " " + artifact["creator"]["lname"] artifact_string += "Creator: " + artifact_object["Creator"] + "\n" hr_incident["Artifacts"] += artifact_string artifacts.append(artifact_object) incident_object["Artifacts"] = artifacts hr_incidents.append(hr_incident) ec_incidents.append(incident_object) ec = {"Resilient.Incidents(val.Id && val.Id === obj.Id)": {"Id": incident_id, "Related": ec_incidents}} title = "Incident " + incident_id + " related incidents" entry = { "Type": entryTypes["note"], "Contents": response, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, hr_incidents), "EntryContext": ec, } return entry else: return "No related incidents found." def related_incidents(client, incident_id): response = client.get("/incidents/" + incident_id + "/related_ex?want_artifacts=true") return response def get_scripts(client: SimpleClient, script_id: str) -> dict[str, Any]: """ Retrieves a single script's enriched data if `script_id` is provided, and retrieves the list of scripts belonging to the IBM QRadar SOAR organization if `script_id` is not provided. """ response = client.get(f"/scripts/{script_id}") demisto.debug(f"get_scripts | {type(response)=} | {response=}") return response def fetch_incidents(client, first_fetch_time: str, fetch_closed: bool): last_fetched_timestamp = demisto.getLastRun() and demisto.getLastRun().get("time") demisto.info(f"fetch_incidents {last_fetched_timestamp=} | {first_fetch_time=}") if not last_fetched_timestamp: if not first_fetch_time: raise DemistoException("First fetch time not provided.") last_fetched_timestamp = to_timestamp(first_fetch_time) args = {"date-created-after": last_fetched_timestamp} # Fetch incident from the last fetched timestamp. resilient_incidents = search_incidents(client, args) demisto_incidents = [] if resilient_incidents: demisto.info( f'fetch_incidents retrieved {len(resilient_incidents)=} | ' f'with IDs: {[incident.get("id") for incident in resilient_incidents]}' ) # Update last_run_time to the latest incident creation time (maximum in milliseconds). last_fetched_timestamp = last_incident_creation_time = max( [_incident.get("create_date") for _incident in resilient_incidents] ) demisto.debug(f"fetch_incidents {last_incident_creation_time=}") for incident in resilient_incidents: # Only fetching non-resolved incidents if `fetch_closed` is disabled. if fetch_closed or (not incident.get("end_date") and incident.get("plan_status") == "A"): # 'A' stands for 'Active' demisto.debug(f"fetch_incidents {incident=}") incident = process_raw_incident(client, incident) demisto_incident = {} demisto_incident["name"] = f'IBM QRadar SOAR incident ID {incident["id"]!s}' demisto_incident["occurred"] = incident.get("discovered_date", None) or incident["create_date"] demisto_incident["rawJSON"] = json.dumps(incident) demisto_incidents.append(demisto_incident) # Increasing by one millisecond in order not to fetch the same incident in the next run. demisto.setLastRun({"time": last_fetched_timestamp + 1}) demisto.incidents(demisto_incidents) def add_note(client: SimpleClient, incident_id: str, note_content: str) -> dict: """ Adds a note to the specified incident. """ body = {"text": {"format": "text", "content": note_content}} return client.post(f"/incidents/{incident_id!s}/comments", body) def add_custom_task( client: SimpleClient, incident_id: str, task_name: str, phase: str, due_date: int | None, description: str, instructions: str, owner_id: str, ) -> dict: """ Adds a custom task to the incident. If task creation was successful, task ID is returned. """ # Initiating with required fields. task_dto: Dict[str, Any] = { "name": task_name, "phase_id": {"name": phase}, "description": description, } # Optional fields. if due_date: task_dto["due_date"] = due_date # Due date in milliseconds timestamp. if instructions: task_dto["instructions"] = instructions if owner_id and owner_id.isdigit(): task_dto["owner_id"] = int(owner_id) elif owner_id: raise DemistoException("Owner ID must be an integer number.") demisto.debug(f"{task_dto=}") return client.post(uri=f"/incidents/{incident_id}/tasks", payload=task_dto) def add_note_command(client, incident_id, note: str, tag_to_ibm: str): response = add_note(client, str(incident_id), "\n".join((note, tag_to_ibm))) demisto.debug(f"add_note_command {response=}") return CommandResults( mark_as_note=True, entry_type=EntryType.NOTE, tags=[tag_to_ibm], outputs_prefix="Resilient.incidentNote", outputs=response, readable_output=f"The note was added successfully to incident {incident_id}\n\n{note} ", ) def add_artifact_command(client, incident_id, artifact_type, artifact_value, artifact_description): body = {"type": artifact_type, "value": artifact_value, "description": {"format": "text", "content": artifact_description}} response = client.post("/incidents/" + str(incident_id) + "/artifacts", body) ec = {"Resilient.incidentArtifact(val.Id && val.Id === obj.Id)": response} entry = { "Type": entryTypes["note"], "Contents": response, "ContentsFormat": formats["json"], "EntryContext": ec, "ReadableContentsFormat": formats["text"], "HumanReadable": f"The artifact was added successfully to incident {incident_id}", } return entry def list_scripts_command(client: SimpleClient, args: dict) -> CommandResults: """ Getting the list of scripts belonging to the IBM QRadar SOAR organization (client instance is org specific), or a specific script if `script_id` argument was provided. """ script_id = args.get("script_id", "") response = get_scripts(client, script_id) script_ids = [] scripts_to_process = [response] if script_id else response.get(SCRIPT_ENTITIES, []) if not script_id and len(scripts_to_process) > 1: # Multiple script to retrieve info for. for script in scripts_to_process: _script_id = script.get("id") if not _script_id: raise DemistoException("Script with ID not found.") script = get_scripts(client, _script_id) # Enriching script's data. script_ids.append(_script_id) demisto.info(f"list_scripts_command received script ids: {script_ids!s}") return CommandResults( outputs_prefix="Resilient.Scripts", outputs=scripts_to_process, # Already processed and enriched with additional data. readable_output=tableToMarkdown( f'{DEMISTO_PARAMS.get("org")} Scripts', scripts_to_process, headers=["id", "name", "description", "language"] ), ) def get_attachment_command(client: SimpleClient, args: dict) -> dict: """ Retrieves an attachment with ID: `args['attachment_id']` from IBM QRadar SOAR. """ name, contents = get_attachment(client, str(args.get("incident_id", "")), str(args.get("attachment_id", ""))) demisto.debug(f"get_attachments_command {name=}") return fileResult(name, contents) def upload_incident_attachment_command(client: SimpleClient, args: dict, tag_to_ibm: str) -> CommandResults: """ Uploads a file from XSOAR to an IBM QRadar SOAR incident. """ incident_id = args.get("incident_id") if not incident_id: raise DemistoException("Incident ID is required.") entry_id = args.get("entry_id") if not entry_id: raise DemistoException("Entry ID is required.") upload_incident_attachment(client, str(incident_id), str(entry_id), tag_to_ibm) return CommandResults(readable_output=f"File was uploaded successfully to {incident_id}.") def delete_incidents_command(client: SimpleClient, args: dict) -> CommandResults: """ Deletes multiple incidents. """ incident_ids: list = argToList(args.get("incident_ids", "")) demisto.info(f"delete_incidents_command {incident_ids=}") response: dict = client.put("/incidents/delete", payload=incident_ids) human_readable: str = ( f"Incidents {incident_ids} were deleted successfully." if response["success"] else f"{response['message']}" ) return CommandResults(readable_output=human_readable) def list_incident_notes_command(client: SimpleClient, args: dict) -> CommandResults: """ Lists an array of open tasks to which the current user is assigned. """ incident_id = str(args.get("incident_id")) demisto.debug(f"list_incident_notes_command {incident_id=}") response = get_incident_notes(client, incident_id) human_readable: str = tableToMarkdown(f"Incident {incident_id} Notes", t=prettify_incident_notes(response)) demisto.debug(f"{response=}") return CommandResults( outputs_prefix="Resilient.IncidentNote", outputs=response, readable_output=human_readable, ) def update_incident_note_command(client: SimpleClient, args: dict) -> CommandResults: """ Updates an incident's comment. """ incident_id, note_id, note_text = ( args.get("incident_id"), args.get("note_id"), args.get("note"), ) demisto.debug(f"update_incident_note_command {incident_id=}, {note_id=}, {note_text=}") body = {"text": {"format": "text", "content": note_text}} response = client.put(f"/incidents/{incident_id}/comments/{note_id}", payload=body) demisto.debug(f"{response=}") return CommandResults(readable_output=f"Successfully updated note ID {note_id} for incident ID {incident_id}") def list_tasks_command(client: SimpleClient) -> CommandResults: """ Lists an array of open tasks to which the current user is assigned. """ response: list = client.get("/tasks") demisto.debug(f"{response=}") tasks_list = [] for incident_tasks_obj in response: tasks_list.extend(incident_tasks_obj.get("tasks")) human_readable: str = tableToMarkdown(name="Open Tasks", t=tasks_list) return CommandResults( outputs_prefix="Resilient.Tasks", outputs=response, readable_output=human_readable, ) def get_task_members_command(client: SimpleClient, args: dict) -> CommandResults: """ Gets the members of a given task by its ID. """ task_id: str = str(args.get("task_id", "")) response = client.get(f"/tasks/{task_id}/members") demisto.debug(f"{response=}") return CommandResults( outputs_prefix="Resilient.Task", outputs=response, readable_output=response.get("content", ""), ) def delete_tasks_command(client: SimpleClient, args: dict) -> CommandResults: """ Deletes a single or multiple tasks. """ task_ids: list = argToList(args.get("task_ids")) if not task_ids: raise DemistoException("No task IDs provided.") response: dict = client.put("/tasks/delete", payload=task_ids) demisto.debug(f"delete_tasks_command {response=}") human_readable = f"Tasks with IDs {task_ids} were deleted successfully." if response["success"] else f"{response['message']}" demisto.debug(f"{response=}") return CommandResults(readable_output=human_readable) def delete_task_members_command(client: SimpleClient, args: dict) -> CommandResults: """ Deletes the members for a given task. """ task_id = args.get("task_id") response = client.delete(f"/tasks/{task_id}/members") demisto.debug(f"{response=}") return CommandResults(readable_output=response.get("content", "")) def list_task_instructions_command(client: SimpleClient, args: dict) -> CommandResults: """ Gets the instructions for a specific task. """ task_id = args.get("task_id") response = client.get(f"/tasks/{task_id}/instructions_ex?text_content_output_format=objects_convert_text") return CommandResults( outputs_prefix="Resilient.Task", outputs=response, readable_output=response.get("content", ""), ) def add_custom_task_command(client: SimpleClient, args: dict) -> CommandResults: """ Adds a custom task to the specified incident. """ demisto.debug(f"add_custom_task_command {args=}") incident_id: str = str(args.get("incident_id", "")) name: str = str(args.get("name", "")) owner_id: str = str(args.get("owner_id", "")) description: str = str(args.get("description", "")) instructions: str = str(args.get("instructions", "")) phase: str = str(args.get("phase", "")) if due_date := args.get("due_date"): due_date = validate_iso_time_format(str(due_date)) due_date = to_timestamp(due_date) else: due_date = None response = add_custom_task(client, incident_id, name, phase, due_date, description, instructions, owner_id) demisto.debug(f"add_custom_task_command {response=}") if task_id := response.get("id"): return CommandResults( outputs_prefix="Resilient.TaskId", outputs=task_id, readable_output=f"Successfully created new task for incident with ID {incident_id}. Task ID: {task_id}", ) return CommandResults(readable_output=f"Could not create a new task: {response.get('message')}") def get_modified_remote_data_command(client: SimpleClient, args: dict) -> GetModifiedRemoteDataResponse: remote_args = GetModifiedRemoteDataArgs(args) last_update = validate_iso_time_format(remote_args.last_update) # In the first run, this value will be set to 1 minute # earlier demisto.debug(f"get-modified-remote-data command {last_update=}") incidents = search_incidents(client, args={"last-modified-after": last_update}) # Casting the incident ID to match the format expected by the server. modified_incident_ids = [str(incident.get("id")) for incident in incidents] demisto.debug(f"get-modified-remote-data command {modified_incident_ids=}") return GetModifiedRemoteDataResponse(modified_incident_ids) def get_remote_data_command(client: SimpleClient, args: dict, tag_to_ibm: str, tag_from_ibm: str) -> GetRemoteDataResponse: """ Args: client (SimpleClient): The IBM Resilient client. args (dict): The command arguments. tag_to_ibm (str): Mirror in tag. tag_from_ibm (str): Mirror out tag. Returns: GetRemoteDataResponse: Structured incident response. """ remote_args = GetRemoteDataArgs(args) # In the first run, this value will be set to 1 minute earlier. last_update_iso = validate_iso_time_format(remote_args.last_update) last_update_timestamp = to_timestamp(last_update_iso) incident_id = remote_args.remote_incident_id demisto.debug(f"get_remote_data_command {incident_id=}") incident = get_incident(client, incident_id, content_format=True) incident = process_raw_incident(client, incident) demisto.debug(f"get_remote_data_command {incident=}") entries = [] # Create note entries. note_entries = incident.get("notes", []) for note_entry in note_entries: demisto.debug(f"get_remote_data_command {note_entry=}") note_modify_date_timestamp = note_entry.get("modify_date") if ( tag_to_ibm not in str(note_entry["text"]) and note_modify_date_timestamp and note_modify_date_timestamp >= last_update_timestamp ): entries.append( { "ContentsFormat": EntryFormat.TEXT, "Type": EntryType.NOTE, "Contents": f"{note_entry.get('text').get('content')}\nAdded By: {note_entry.get('created_by', '')}\n", "Tags": [tag_from_ibm], "Note": True, } ) # Create file entries attachment_entries = incident.get("attachments", []) for attachment_entry in attachment_entries: demisto.debug(f"get_remote_data_command {attachment_entry=}") attachment_create_time = attachment_entry.get("Create Time") if ( tag_to_ibm not in attachment_entry.get("Name", "") and attachment_create_time and attachment_create_time >= last_update_timestamp ): file_name, content = get_attachment(client, incident_id, attachment_entry.get("ID")) file_entry = fileResult(filename=file_name, data=content, file_type=EntryType.ENTRY_INFO_FILE) entries.append(file_entry) # Handling remote incident resolution. 'C' stands for 'Closed' if DEMISTO_PARAMS.get("close_xsoar_incident", False) and incident.get("end_date") and incident.get("plan_status") == "C": resolution_id = incident.get("resolution_id") if resolution_id is not None: closing_entry = handle_incoming_incident_resolution( incident_id=incident_id, resolution_id=int(resolution_id), resolution_summary=incident.get("resolution_summary", ""), ) entries.append(closing_entry) # Handling open and remote incident re-opening. elif not incident.get("end_date") and incident.get("plan_status") == "A": reopening_entry = handle_incoming_incident_reopening(incident_id=incident_id) entries.append(reopening_entry) mirrored_data = {} mirrored_data["rawJSON"] = json.dumps(incident) demisto.debug(f"get_remote_data_command mirrored_object={incident}") return GetRemoteDataResponse(mirrored_object=incident, entries=entries) def update_remote_system_command(client: SimpleClient, args: dict, tag_to_ibm: str) -> str: remote_args = UpdateRemoteSystemArgs(args) incident_id = remote_args.remote_incident_id demisto.debug( f"update_remote_system_command {incident_id=} | {remote_args.incident_changed=}" f" {remote_args.entries=} | {remote_args.delta=} | {remote_args.data=} | {remote_args.inc_status}" ) if remote_args.incident_changed and remote_args.delta: update_dto = prepare_incident_update_dto_for_mirror(client, incident_id, remote_args.delta) update_incident(client, incident_id, update_dto) else: demisto.debug(f"Skipping updating remote incident fields [{remote_args.remote_incident_id}] as it is not new nor changed") entries = remote_args.entries if entries: for entry in entries: demisto.debug(f"update_remote_system_command {entry=}") entry_id = entry.get("id", "") entry_type = entry.get("type", "") entry_tags = entry.get("tags", []) demisto.debug(f"update_remote_system {entry_id=} | {entry_type=} | {entry_tags=}") if entry_type == EntryType.NOTE and tag_to_ibm in entry_tags: add_note(client, incident_id, entry.get("Contents")) elif entry_type == EntryType.FILE and tag_to_ibm in entry_tags: upload_incident_attachment(client, incident_id, entry_id, tag_to_ibm) return incident_id def get_mapping_fields_command() -> GetMappingFieldsResponse: ibm_qradar_incident_type_scheme = SchemeTypeMapping( type_name=IBM_QRADAR_SOAR_INCIDENT_SCHEMA_NAME, fields=IBM_QRADAR_INCIDENT_FIELDS, ) fields_copy = [] for field in IBM_QRADAR_INCIDENT_FIELDS: fields_copy.append(field) for field in fields_copy: ibm_qradar_incident_type_scheme.add_field(name=field, description=IBM_QRADAR_INCIDENT_FIELDS[field].get("description")) return GetMappingFieldsResponse([ibm_qradar_incident_type_scheme]) def test_module(client: SimpleClient, fetch_time: str, tag_to_ibm=DEFAULT_TAG_TO_IBM, tag_from_ibm=DEFAULT_TAG_FROM_IBM) -> str: """ Verify client connectivity and the fetch_time parameter are according to the standards, if exists. Returns: 'ok' if all tests passed, anything else will fail the test. """ # Making a request to the client's base URL to retrieve information about the organization. client.get(uri="") # Testing fetch_time parameter's value. if fetch_time: try: datetime.strptime(fetch_time, TIME_FORMAT) except ValueError: raise DemistoException( "Invalid first fetch timestamp format, should be (YYYY-MM-DDTHH:MM:SSZ). For example: 2020-02-02T19:00:00Z" ) # Testing tags if tag_from_ibm == tag_to_ibm: raise DemistoException(f"Tag *to* IBM (`{tag_to_ibm}`) and Tag *from* IBM (`{tag_from_ibm}`) cannot have the same value.") return "ok" """ EXECUTION CODE """ def get_client(): # pragma: no cover opts_dict = { "host": SERVER, "port": PORT, "cafile": os.environ.get("SSL_CERT_FILE") if USE_SSL else "false", "org": ORG_NAME, } if API_KEY_ID and API_KEY_SECRET: opts_dict.update({"api_key_id": API_KEY_ID, "api_key_secret": API_KEY_SECRET}) elif USERNAME and PASSWORD: opts_dict.update({"email": USERNAME, "password": PASSWORD}) else: return_error("Credentials were not provided. Please configure API key ID and API key secret") resilient_client = resilient.get_client(opts=opts_dict) resilient_client.request_max_retries = DEFAULT_RETRIES return resilient_client def main(): # pragma: no cover params = demisto.params() fetch_time = validate_iso_time_format(params.get("fetch_time", "")) client = get_client() # Disable SDK logging warning messages integration_logger = logging.getLogger("resilient") # type: logging.Logger integration_logger.propagate = False tag_to_ibm = params.get("tag_to_ibm", DEFAULT_TAG_TO_IBM) tag_from_ibm = params.get("tag_from_ibm", DEFAULT_TAG_FROM_IBM) demisto.debug(f"main {tag_from_ibm=} | {tag_to_ibm=}") try: command = demisto.command() args = demisto.args() demisto.info(f"main {command=} | {args=}") if command == "test-module": # Checks if there is an authenticated session return_results(test_module(client, fetch_time, tag_to_ibm, tag_from_ibm)) elif command == "fetch-incidents": fetch_incidents(client, fetch_time, params.get("fetch_closed", False)) elif command == "rs-search-incidents": return_results(search_incidents_command(client, args)) elif command == "rs-update-incident": return_results(update_incident_command(client, args)) elif command == "rs-incidents-get-members": return_results(get_members_command(client, args["incident-id"])) elif command == "rs-get-incident": return_results(get_incident_command(client, args["incident-id"])) elif command == "rs-incidents-update-member": return_results(set_member_command(client, args["incident-id"], args["members"])) elif command == "rs-incidents-get-tasks": return_results(get_tasks_command(client, args["incident-id"])) elif command == "rs-get-users": return_results(get_users_command(client)) elif command == "rs-close-incident": return_results(close_incident_command(client, args["incident-id"])) elif command == "rs-create-incident": return_results(create_incident_command(client, args)) elif command == "rs-incident-artifacts": return_results(incident_artifacts_command(client, args["incident-id"])) elif command == "rs-incident-attachments": return_results(incident_attachments_command(client, args["incident-id"])) elif command == "rs-get-attachment": return_results(get_attachment_command(client, args)) elif command == "rs-upload-incident-attachment": return_results(upload_incident_attachment_command(client, args, tag_to_ibm)) elif command == "rs-related-incidents": return_results(related_incidents_command(client, args["incident-id"])) elif command == "rs-add-note": return_results(add_note_command(client, args["incident-id"], args["note"], tag_to_ibm)) elif command == "rs-add-artifact": demisto.results( add_artifact_command( client, args["incident-id"], args["artifact-type"], args["artifact-value"], args.get("artifact-description"), ) ) elif command == "rs-list-scripts": return_results(list_scripts_command(client, args)) elif command == "rs-delete-incidents": return_results(delete_incidents_command(client, args)) elif command == "rs-list-incident-notes": return_results(list_incident_notes_command(client, args)) elif command == "rs-update-incident-note": return_results(update_incident_note_command(client, args)) elif command == "rs-list-tasks": return_results(list_tasks_command(client)) elif command == "rs-update-task": return_results(update_task_command(client, args)) elif command == "rs-get-task-members": return_results(get_task_members_command(client, args)) elif command == "rs-delete-tasks": return_results(delete_tasks_command(client, args)) elif command == "rs-delete-task-members": return_results(delete_task_members_command(client, args)) elif command == "rs-list-task-instructions": return_results(list_task_instructions_command(client, args)) elif command == "rs-add-custom-task": return_results(add_custom_task_command(client, args)) elif command == "get-modified-remote-data": return_results(get_modified_remote_data_command(client, args)) elif command == "get-remote-data": return_results(get_remote_data_command(client, args, tag_to_ibm, tag_from_ibm)) elif command == "update-remote-system": return_results(update_remote_system_command(client, args, tag_to_ibm)) elif command == "get-mapping-fields": return_results(get_mapping_fields_command()) except Exception as e: LOG(str(e)) LOG.print_log() raise if __name__ in ("__main__", "__builtin__", "builtins"): main()