import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * """ IMPORTS """ import base64 import hashlib import hmac import json import uuid from datetime import timedelta from urllib.error import HTTPError from xml.etree import ElementTree import requests """ GLOBALS/PARAMS """ BASE_URL = demisto.params().get("baseUrl") ACCESS_KEY = demisto.params().get("accessKey") SECRET_KEY = demisto.params().get("secretKey") or demisto.params().get("secretKey_creds") if isinstance(SECRET_KEY, dict): SECRET_KEY = SECRET_KEY.get("password", "") APP_ID = demisto.params().get("appId") APP_KEY = demisto.params().get("appKey") or demisto.params().get("appKey_creds", {}) if isinstance(APP_KEY, dict): APP_KEY = APP_KEY.get("password", "") USE_SSL = None # assigned in determine_ssl_usage PROXY = bool(demisto.params().get("proxy")) # Flags to control which type of incidents are being fetched FETCH_PARAMS = argToList(demisto.params().get("incidentsToFetch")) FETCH_ALL = "All" in FETCH_PARAMS FETCH_URL = "Url" in FETCH_PARAMS or FETCH_ALL FETCH_ATTACHMENTS = "Attachments" in FETCH_PARAMS or FETCH_ALL FETCH_IMPERSONATIONS = "Impersonation" in FETCH_PARAMS or FETCH_ALL FETCH_HELD_MESSAGES = "Held Messages" in FETCH_PARAMS or FETCH_ALL # Used to refresh token / discover available auth types / login EMAIL_ADDRESS = demisto.params().get("email") or demisto.params().get("credentials", {}) if isinstance(EMAIL_ADDRESS, dict): EMAIL_ADDRESS = EMAIL_ADDRESS.get("identifier", "") PASSWORD = demisto.params().get("password") or demisto.params().get("credentials", {}) if isinstance(PASSWORD, dict): PASSWORD = PASSWORD.get("password", "") FETCH_DELTA = int(demisto.params().get("fetchDelta", 24)) MAX_FETCH = arg_to_number(demisto.params().get("max_fetch", 100)) or 100 if MAX_FETCH > 200: raise DemistoException("The maximum fetch limit cannot exceed 200. Please enter a lower value.") CLIENT_ID = demisto.params().get("client_id") CLIENT_SECRET = demisto.params().get("client_secret", {}).get("password") if demisto.params().get("client_secret") else None USE_OAUTH2 = bool(CLIENT_ID and CLIENT_SECRET) TOKEN_OAUTH2 = "" DEFAULT_POLICY_TYPE = "blockedsenders" # v2 blocked-senders endpoints (OAuth2 only); the other policy types still use the v1 /api/policy routes. BLOCKED_SENDERS_V2_ENDPOINT = "/policy-management/cloud-gateway/v1/blocked-senders/policies" BLOCKED_SENDERS_HR_HEADERS = ["Policy ID", "Sender", "Receiver", "Bidirectional", "Start", "End"] LOG(f"command is {demisto.command()}") PAGE_SIZE_MAX = 100 DEFAULT_PAGE_SIZE = 50 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S+00:00" # default query xml template for test module default_query_xml = ' \n\ \n\ \n\ \n\ \n\ attachmentcount\n\ status\n\ subject\n\ size\n\ receiveddate\n\ displayfrom\n\ id\n\ displayto\n\ smash\n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ ' """ API COMMUNICATION FUNCTIONS""" def handle_error_response(failure_response, error_key="errors"): """ Safely extract error details from Mimecast API failure response. Args: failure_response: The 'fail' field from API response error_key: The key to extract from error object (default: "errors", can be "message") Returns: Error details suitable for json.dumps() Raises: Exception with formatted error message """ if isinstance(failure_response, list) and failure_response: error_details = failure_response[0].get(error_key, failure_response[0]) else: error_details = failure_response raise Exception(json.dumps(error_details)) def request_with_pagination( api_endpoint: str, data: list, response_param: str = None, limit: int = 100, page: int = None, page_size: int = None, use_headers: bool = False, is_file: bool = False, ): """ Creates paging response for relevant commands. """ headers = {} if page and page_size: limit = page * page_size pagination = {"pageSize": limit} payload = {"meta": {"pagination": pagination}} # type: Dict[str, Any] if data and data != [{}]: payload["data"] = data if use_headers: headers = generate_user_auth_headers(api_endpoint) response = http_request("POST", api_endpoint, payload, headers=headers, is_file=is_file) next_page = str(response.get("meta", {}).get("pagination", {}).get("next", "")) results: list[Any] = [] while True: if failure_response := response.get("fail"): handle_error_response(failure_response) if response_param: response_data = response.get("data")[0].get(response_param) else: response_data = response.get("data") for entry in response_data: # If returning this log will not exceed the specified limit if not limit or len(results) < limit: results.append(entry) # If limit is reached or there are no more pages if not next_page or (limit and len(results) >= limit): break pagination = { "page_size": page_size, # type: ignore "pageToken": next_page, # type: ignore } payload["meta"]["pagination"] = pagination response = http_request("POST", api_endpoint, payload, headers=headers) next_page = str(response.get("meta", {}).get("pagination", {}).get("next", "")) if page and page_size: return results[(-1 * page_size) :], page_size return results, len(results) def fetch_logs_with_pagination( api_endpoint: str, data: list, response_param: str = None, limit: int = PAGE_SIZE_MAX, dedup_messages: list | None = None, current_next_page: str = "", log_type: str = "", ): """ Generic function for fetching logs with pagination and deduplication support. Args: api_endpoint: The API endpoint to call data: The data payload to send response_param: The parameter name in response containing the logs (e.g., 'clickLogs', 'attachmentLogs') If None, uses response.get("data") directly limit: Maximum number of results to fetch dedup_messages: List of message IDs to deduplicate against current_next_page: Token for continuing from a previous pagination log_type: Type of log for ID generation (e.g., 'url', 'attachment', 'impersonation', 'held_message') Returns: Tuple of (results, len_of_results, next_page) """ demisto.debug(f"fetch_logs_with_pagination: Starting request to {api_endpoint}") demisto.debug(f"fetch_logs_with_pagination: {limit=}, dedup_count={len(dedup_messages or [])}, {current_next_page=}") demisto.debug(f"fetch_logs_with_pagination: Request data={data}") payload: dict[str, Any] = {"meta": {}, "data": data} results = [] dropped = 0 next_page = current_next_page or "" dedup_messages = dedup_messages or [] for iteration in range(1, 11): pagination = {"pageSize": limit} if next_page: demisto.debug(f"fetch_logs_with_pagination: Iteration {iteration} - Using next_page token (pagination continuation)") pagination = {"pageSize": limit, "pageToken": next_page} # type: ignore else: demisto.debug(f"fetch_logs_with_pagination: Iteration {iteration} - Starting new query (no next_page token)") payload["meta"]["pagination"] = pagination demisto.debug(f"fetch_logs_with_pagination: Iteration {iteration} - Sending HTTP request") response = http_request("POST", api_endpoint, payload, headers={}) if failure_response := response.get("fail"): handle_error_response(failure_response) # Extract response data based on response_param if response_param: data_list = response.get("data", []) response_data = data_list[0].get(response_param, []) if data_list else [] else: response_data = response.get("data", []) demisto.debug(f"fetch_logs_with_pagination: Iteration {iteration} - Received {len(response_data)} entries from API") for entry in response_data: entry_id = generate_log_id(entry, log_type) if log_type else entry.get("id") # Dedup for fetch - only if dedup_messages is provided and entry has an id if dedup_messages and entry_id and entry_id in dedup_messages: dropped += 1 demisto.debug(f"fetch_logs_with_pagination: Dropped {entry_id} (duplicate)") else: results.append(entry) next_page = str(response.get("meta", {}).get("pagination", {}).get("next", "")) demisto.debug( f"fetch_logs_with_pagination: Iteration {iteration} - Results so far: {len(results)}, " f"next_page_exists={bool(next_page)}" ) if not next_page: demisto.debug("fetch_logs_with_pagination: No more pages - pagination complete") break if limit and len(results) >= limit: demisto.debug(f"fetch_logs_with_pagination: Limit reached ({len(results)} >= {limit}) - stopping pagination") break demisto.debug( f"fetch_logs_with_pagination: Final results - total={len(results)}, dropped={dropped}, has_next_page={bool(next_page)}" ) return results, len(results), next_page def http_request(method, api_endpoint, payload=None, params={}, user_auth=True, is_file=False, headers={}, data=None): is_user_auth = True url = BASE_URL + api_endpoint # 3 types of auth, user, non user and OAuth2 if USE_OAUTH2: if TOKEN_OAUTH2: headers["Authorization"] = f"Bearer {TOKEN_OAUTH2}" headers["Accept"] = "application/json" headers["Content-Type"] = "application/json" elif user_auth: headers = headers or generate_user_auth_headers(api_endpoint) else: # This type of auth is only supported for basic commands: login/discover/refresh-token is_user_auth = False auth = base64.b64encode((EMAIL_ADDRESS + ":" + PASSWORD).encode("utf-8")).decode() auth_type = "Basic-Cloud" auth_header = auth_type + " " + auth headers = {"x-mc-app-id": APP_ID, "Content-Type": "application/json", "Authorization": auth_header} demisto.debug( f"running {method} request with url={url}, params={json.dumps(params)}, data={json.dumps(payload)}," f"is user auth={is_user_auth}" ) try: res = requests.request(method, url, verify=USE_SSL, params=params, headers=headers, json=payload, data=data) res.raise_for_status() if is_file: return res return res.json() except (HTTPError, requests.exceptions.HTTPError) as e: LOG(e) response = getattr(e, "response", None) status_code = getattr(response, "status_code", None) if status_code == 418: if not APP_ID or not EMAIL_ADDRESS or not PASSWORD: raise Exception( "Credentials provided are expired, could not automatically refresh tokens." " App ID + Email Address " "+ Password are required." ) else: # Attempt to parse v2 error envelope: {"error": [{"code": ..., "message": ...}]} try: error_body = response.json() if response is not None else None if error_body and (errors := error_body.get("error")): messages = "; ".join(err.get("message", str(err)) for err in errors) raise DemistoException(messages) from e except (ValueError, AttributeError): demisto.debug("Could not parse v2 error body; re-raising original HTTPError") raise except Exception as e: LOG(e) raise def token_oauth2_request() -> tuple[str, int]: """Fetch a new OAuth2 token and return (access_token, expires_in).""" api_endpoint = "/oauth/token" headers = {"Content-Type": "application/x-www-form-urlencoded"} data = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "grant_type": "client_credentials"} response = http_request("POST", api_endpoint, user_auth=False, headers=headers, data=data) if failure_response := response.get("fail"): handle_error_response(failure_response, error_key="message") return response.get("access_token"), int(response.get("expires_in", 1799)) def search_message_request(args): """ Builds payload for the request of search message command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ search_reason = args.get("search_reason") from_date = arg_to_datetime(args.get("from_date")).isoformat() if args.get("from_date") else None # type: ignore to_date = arg_to_datetime(args.get("to_date")).isoformat() if args.get("to_date") else None # type: ignore message_id = args.get("message_id") advanced = { "senderIP": args.get("sender_ip"), "to": args.get("to"), "from": args.get("from"), "subject": args.get("subject"), "route": args.get("route"), } advanced_is_none = all(value is None for value in advanced.values()) payload = {"data": [{"start": from_date, "end": to_date, "searchReason": search_reason}]} if advanced_is_none and message_id is None: raise Exception("Advanced Track And Trace Options or message ID must be given in order to execute the command.") elif advanced_is_none: payload.get("data")[0].update({"messageId": message_id}) # type: ignore elif message_id is None: payload.get("data")[0].update({"advancedTrackAndTraceOptions": advanced}) # type: ignore else: raise Exception("Only one of message id and advance options can contain value.") return http_request(method="POST", api_endpoint="/api/message-finder/search", payload=payload) def get_message_info_request(id): """ Builds payload for the request of get message info command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ payload = {"data": [{"id": id}]} return http_request(method="POST", api_endpoint="/api/message-finder/get-message-info", payload=payload) def list_held_messages_request(args): """ Builds payload for the request of list hold messages command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ admin = argToBoolean(args.get("admin")) from_date = arg_to_datetime(args.get("from_date")).isoformat() if args.get("from_date") else None # type: ignore to_date = arg_to_datetime(args.get("to_date")).isoformat() if args.get("to_date") else None # type: ignore value = args.get("value", "") field_name = args.get("field_name", "") # Backward compatibility: map old 'reason_code' to new 'reasonCode' if field_name == "reason_code": field_name = "reasonCode" limit = arg_to_number(args.get("limit")) or 20 page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) data = [{"admin": admin, "start": from_date, "end": to_date}] if field_name or value: data[0].update({"searchBy": {"fieldName": field_name, "value": value}}) return request_with_pagination( api_endpoint="/api/gateway/get-hold-message-list", data=data, limit=limit, page=page, page_size=page_size ) def reject_held_message_request(args): """ Builds payload for the request of reject hold messages command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ ids = argToList(args.get("ids")) message = args.get("message") reason_type = args.get("reason_type") notify = argToBoolean(args.get("notify")) payload = {"data": [{"message": message, "ids": ids, "reasonType": reason_type, "notify": notify}]} return http_request("POST", api_endpoint="/api/gateway/hold-reject", payload=payload) def release_held_message_request(id): """ Builds payload for the request of release hold messages command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ payload = {"data": [{"id": id}]} return http_request("POST", api_endpoint="/api/gateway/hold-release", payload=payload) def search_processing_message_request(args): """ Builds payload for the request of search processing message command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ sort_order = args.get("sort_order") from_date = arg_to_datetime(args.get("from_date")).isoformat() if args.get("from_date") else None # type: ignore to_date = arg_to_datetime(args.get("to_date")).isoformat() if args.get("to_date") else None # type: ignore attachments = argToBoolean(args.get("attachments")) if args.get("attachments") else None value = args.get("value") field_name = args.get("field_name") route = args.get("route") limit = arg_to_number(args.get("limit")) or 20 page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) data = [ { "sortOrder": sort_order, } ] if to_date: data[0].update({"end": to_date}) if from_date: data[0].update({"start": from_date}) if value or field_name: data[0].update({"searchBy": {"fieldName": field_name, "value": value}}) if attachments or route: data[0].update({"filterBy": [{"attachments": attachments, "route": route}]}) return request_with_pagination( api_endpoint="/api/gateway/find-processing-messages", data=data, response_param="messages", limit=limit, page=page, page_size=page_size, ) def list_email_queues_request(args): """ Builds payload for the request of list email queues command. Args: args: arguments given to command. Returns: the payload to be sent to the API. """ from_date = arg_to_datetime(args.get("from_date")).isoformat() if args.get("from_date") else None # type: ignore to_date = arg_to_datetime(args.get("to_date")).isoformat() if args.get("to_date") else None # type: ignore payload = {"data": [{"start": from_date, "end": to_date}]} return http_request("POST", api_endpoint="/api/email/get-email-queues", payload=payload) """ HELPER FUNCTIONS """ def determine_ssl_usage(): global USE_SSL old_insecure = demisto.params().get("insecure", None) if old_insecure: USE_SSL = bool(old_insecure) return USE_SSL = not demisto.params().get("new_insecure") def epoch_seconds(d=None): """ Return the number of seconds for given date. If no date, return current. """ if not d: d = datetime.utcnow() return int((d - datetime.utcfromtimestamp(0)).total_seconds()) def auto_refresh_token(): """ Check if we have a valid token, if not automatically renew validation time for 3 days when necessary params are provided """ if APP_ID and EMAIL_ADDRESS and PASSWORD: integration_context = demisto.getIntegrationContext() last_update_ts = integration_context.get("token_last_update") current_ts = epoch_seconds() if (last_update_ts and current_ts - last_update_ts > 60 * 60 * 24 * 3 - 1800) or last_update_ts is None: refresh_token_request() current_ts = epoch_seconds() demisto.setIntegrationContext({"token_last_update": current_ts}) def updating_token_oauth2(): """Ensures the OAuth2 token is up to date, refreshing it if necessary.""" global TOKEN_OAUTH2 integration_context = demisto.getIntegrationContext() current_ts = epoch_seconds() last_update_ts = integration_context.get("last_update") expires_in = integration_context.get("expires_in", 1799) if last_update_ts is None or (current_ts - last_update_ts >= expires_in - 60): TOKEN_OAUTH2, expires_in = token_oauth2_request() if TOKEN_OAUTH2: demisto.setIntegrationContext({"value": TOKEN_OAUTH2, "last_update": current_ts, "expires_in": expires_in}) else: TOKEN_OAUTH2 = integration_context.get("value") def generate_user_auth_headers(api_endpoint): # type: (str) -> dict """ Generate headers for a request Args: api_endpoint: The request's endpoint Returns: A dict of headers for the request """ # Generate request header values request_id = str(uuid.uuid4()) hdr_date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S") + " UTC" # DataToSign is used in hmac_sha1 dataToSign = ":".join([hdr_date, request_id, api_endpoint, APP_KEY]) # Create the HMAC SHA1 of the Base64 decoded secret key for the Authorization header hmac_sha1 = hmac.new(base64.b64decode(SECRET_KEY), dataToSign.encode(), digestmod=hashlib.sha1).digest() # Use the HMAC SHA1 value to sign the hdrDate + ":" requestId + ":" + URI + ":" + appkey signature = base64.b64encode(hmac_sha1).rstrip() # Create request headers headers = { "Authorization": "MC " + ACCESS_KEY + ":" + signature.decode(), "x-mc-app-id": APP_ID, "x-mc-date": hdr_date, "x-mc-req-id": request_id, "Content-Type": "application/json", } return headers def parse_query_args(args): query_xml = default_query_xml if args.get("pageSize"): query_xml = query_xml.replace('page-size="25"', 'page-size="' + args.get("pageSize") + '"') if args.get("startRow"): query_xml = query_xml.replace('startrow="0"', 'startrow="' + args.get("startRow") + '"') if args.get("active") == "true": query_xml = query_xml.replace('active="false"', 'active="true"') if args.get("body"): query_xml = query_xml.replace("", "(body: " + args.get("body") + ")") if args.get("subject"): query_xml = query_xml.replace("", "subject: " + args.get("subject") + "") if args.get("text"): query_xml = query_xml.replace("", "" + args.get("text") + "") if args.get("date"): query_xml = query_xml.replace('', '') if args.get("dateTo") or args.get("dateFrom"): raise Exception("Cannot use both date and dateFrom/dateTo arguments") date_to = "" date_from = "" if args.get("dateTo"): date_to = args.get("dateTo") if args.get("dateFrom"): date_from = args.get("dateFrom") if date_to and date_from: query_xml = query_xml.replace( '', '' ) elif date_from: query_xml = query_xml.replace('', '') elif date_to: query_xml = query_xml.replace('', '') sent_from = "" sent_to = "" if args.get("sentFrom"): sent_from = args.get("sentFrom") if args.get("sentTo"): sent_to = args.get("sentTo") if sent_from and sent_to: query_xml = query_xml.replace( "", f'{sent_from}{sent_to}' ) elif sent_from: query_xml = query_xml.replace("", '' + sent_from + "") elif sent_to: query_xml = query_xml.replace("", '' + sent_to + "") query_xml = query_xml.replace("", "") # no empty tag if args.get("attachmentText"): query_xml = query_xml.replace("", args.get("attachmentText") + "") if args.get("attachmentType"): query_xml = query_xml.replace('', '') return query_xml def build_recipient_info(recipient_info: dict): """ Builds markdown table for recipient info part of the response for get-message-info command """ message_info = recipient_info.get("messageInfo", {}) meta_info = recipient_info.get("recipientMetaInfo", {}) message_info.update(meta_info) headers = { "fromEnv": "From (Header)", "remoteIp": "Remote Ip", "senderIP": "IP Address", "remoteHost": "Remote Host", "encryptionInfo": "Recipient Encryption Info", } return tableToMarkdown( "Recipient Info", t=message_info, headerTransform=lambda header: headers.get(header) if header in headers else header.capitalize(), removeNull=True, headers=["fromHeader", "subject", "sent", "remoteIp", "remoteHost", "encryptionInfo"], ) def build_delivered_message(delivered_messgae: dict, to: List): """ Builds markdown table for delivered message part of the response for get-message-info command Args: to: list of recipients that received the message. """ markdown_per_recipient = "### Delivered Message Info\n" for to_mail in to: delivered = delivered_messgae.get(to_mail, {}) message_info = delivered.get("messageInfo", {}) to_cc_transformer = JsonTransformer(func=lambda data: ", ".join(data)) table_json_transformer = {"to": to_cc_transformer, "cc": to_cc_transformer} markdown_per_recipient += tableToMarkdown( to_mail, t=message_info, headerTransform=lambda header: header.capitalize(), json_transform_mapping=table_json_transformer, removeNull=True, headers=["to", "cc", "subject", "sent"], ) return markdown_per_recipient def build_retention_info(retention_info: dict): """ Builds markdown table for retention info part of the response for get-message-info command """ arr_transformer = JsonTransformer(func=lambda arr: ", ".join(arr)) table_json_transformer = { "litigationHoldInfo": arr_transformer, "fbrStamps": arr_transformer, "smartTags": arr_transformer, "fbrExpireCheck": arr_transformer, "audits": arr_transformer, } return tableToMarkdown( "Retention Info", t=retention_info, headerTransform=string_to_table_header, json_transform_mapping=table_json_transformer, removeNull=True, ) def build_spam_info(spam_info: dict): """ Builds markdown table for spam info part of the response for get-message-info command """ spam_processing_detail = spam_info.get("spamProcessingDetail", {}) spam_info.update(spam_processing_detail) spam_info.pop("spamProcessingDetail", None) headers = {"spamScore": "Spam Score", "detectionLevel": "Spam Detection Level", "permittedSender": "PermittedSender"} return tableToMarkdown( "Spam Info", t=spam_info, headerTransform=lambda header: headers.get(header) if header in headers else header.capitalize(), removeNull=True, ) def transformer_get_value(value): """ Returns a transformer function to use in table_to_markdown function to get a value from a dict in a cell. Args: value: the value key to get his value from the dict. Returns: transformer function """ def transformer(dict_value): return dict_value.get(value) return transformer def build_get_message_info_outputs(outputs: dict): """ Args: response: response from API Returns: outputs dictionary without dynamic keys. """ delivered_message = outputs.get("deliveredMessage", {}) result_emails = [] for email in delivered_message: info_for_mail = delivered_message.get(email) info_for_mail.update({"mail_address": email}) result_emails.append(info_for_mail) outputs.update({"deliveredMessage": result_emails}) def build_get_message_info_for_specific_id(id, show_recipient_info, show_delivered_message, show_retention_info, show_spam_info): """ Args: id: message id to search. show_recipient_info: boolean deciding if to show recipient info in the readable output. show_delivered_message: boolean deciding if to show delivered info in the readable output. show_retention_info: boolean deciding if to show terention info in the readable output. show_spam_info: boolean deciding if to show spam info in the readable output. Returns: CommandResults object with data for the specific id. """ total_markdown = "" outputs = {} response = get_message_info_request(id) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) response_data = response.get("data")[0] recipient_info = response_data.get("recipientInfo", {}) delivered_message = response_data.get("deliveredMessage", {}) retention_info = response_data.get("retentionInfo", {}) spam_info = response_data.get("spamInfo", {}) to_list = recipient_info.get("messageInfo", {}).get("to", []) outputs.update({"status": response_data.get("status", "")}) outputs.update({"id": response_data.get("id", "")}) total_markdown += tableToMarkdown("Message Information", t=outputs) if show_recipient_info: total_markdown += build_recipient_info(recipient_info) outputs.update({"recipientInfo": recipient_info}) if show_delivered_message: total_markdown += build_delivered_message(delivered_message, to_list) outputs.update({"deliveredMessage": delivered_message}) if show_retention_info: total_markdown += build_retention_info(retention_info) outputs.update({"retentionInfo": retention_info}) if show_spam_info: total_markdown += build_spam_info(spam_info) outputs.update({"spamInfo": spam_info}) build_get_message_info_outputs(outputs) return CommandResults( outputs_prefix="Mimecast.MessageInfo", outputs_key_field="id", readable_output=total_markdown, outputs=outputs, raw_response=response, ) """COMMANDS """ def test_module(): if USE_OAUTH2: list_policies_command({"policyType": "blockedsenders", "limit": 1}) return "ok" if ACCESS_KEY: list_managed_url() return "ok" raise Exception( "Cannot test a valid connection without the Client ID and Client Secret parameters for API 2.0\ or without the Access Key parameter for API 1.0." ) def parse_queried_fields(query_xml: str) -> tuple[str, ...]: if not query_xml: return () if not (fields := ElementTree.fromstring(query_xml).find(".//return-fields")): # noqa:S314 - argument set by user demisto.debug("could not find a 'return-fields' section - will only return default fields") return () return tuple(field.text for field in fields if field is not None and field.text) DEFAULT_QUERY_KEYS = frozenset(("subject", "displayfrom", "displayto", "receiveddate", "size", "attachmentcount", "status", "id")) def query(args: dict): if args.get("queryXml"): query_xml = args.get("queryXml", "") else: query_xml = parse_query_args(args) additional_keys = sorted(set(parse_queried_fields(query_xml)).difference(DEFAULT_QUERY_KEYS)) # non-default keys in query) headers = [ "Subject", "Display From", "Display To", "Received Date", "Size", "Attachment Count", "Status", "ID", ] + additional_keys contents = [] messages_context = [] limit = arg_to_number(args.get("limit")) or 20 page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) if args.get("dryRun") == "true": return query_xml # API request demands admin boolean, since we don't have any other support but admin we simply pass true. data = [{"admin": True, "query": query_xml}] messages, _ = request_with_pagination( api_endpoint="/api/archive/search", data=data, response_param="items", limit=limit, page=page, page_size=page_size ) for message in messages: additional_dict = {k: message[k] for k in additional_keys} contents.append( { "Subject": message.get("subject"), "Display From": message.get("displayfrom"), "Display To": message.get("displayto"), "Received Date": message.get("receiveddate"), "Size": message.get("size"), "Attachment Count": message.get("attachmentcount"), "Status": message.get("status"), "ID": message.get("id"), } | additional_dict ) messages_context.append( { "Subject": message.get("subject"), "Sender": message.get("displayfrom"), "Recipient": message.get("displayto"), "ReceivedDate": message.get("receiveddate"), "Size": message.get("size"), "AttachmentCount": message.get("attachmentcount"), "Status": message.get("status"), "ID": message.get("id"), } | additional_dict ) return { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast archived emails", contents, headers), "EntryContext": {"Mimecast.Message(val.ID && val.ID == obj.ID)": messages_context}, } def url_decode(): headers = [] # type: List[str] contents = {} context = {} protected_url = demisto.args().get("url") decoded_url = url_decode_request(protected_url) contents["Decoded URL"] = decoded_url context[outputPaths["url"]] = {"Data": protected_url, "Mimecast": {"DecodedURL": decoded_url}} results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast Decoded URL:", contents, headers), "EntryContext": context, } return results def url_decode_request(url): # Setup required variables api_endpoint = "/api/ttp/url/decode-url" payload = {"data": [{"url": url}]} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) if not response.get("data")[0].get("url"): raise Exception("No URL has been returned from the service") return response.get("data")[0].get("url") def list_blocked_sender_policies_command(args): headers = ["Policy ID", "Sender", "Reciever", "Bidirectional", "Start", "End"] contents = [] context = {} policy_id = args.get("policyID") title = "Mimecast list blocked sender policies: \n These are the existing blocked sender Policies:" if policy_id: title = "Mimecast Get Policy" policies_list = get_policy_request(policy_type="blockedsenders", policy_id=policy_id) policies_context = [] for policy_list in policies_list: policy = policy_list.get("policy", {}) sender = policy.get("from", {}) reciever = policy.get("to", {}) contents.append( { "Policy ID": policy_list["id"], "Sender": { "Group": sender.get("groupId"), "Email Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Reciever": { "Group": reciever.get("groupId"), "Email Address": reciever.get("emailAddress"), "Domain": reciever.get("emailDomain"), "Type": reciever.get("type"), }, "Bidirectional": policy.get("bidirectional"), "Start": policy.get("fromDate"), "End": policy.get("toDate"), } ) policies_context.append( { "ID": policy_list["id"], "Sender": { "Group": sender.get("groupId"), "Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Reciever": { "Group": reciever.get("groupId"), "Address": reciever.get("emailAddress"), "Domain": reciever.get("emailDomain"), "Type": reciever.get("type"), }, "Bidirectional": policy.get("bidirectional"), "FromDate": policy.get("fromDate"), "ToDate": policy.get("toDate"), } ) context["Mimecast.Policy(val.ID && val.ID == obj.ID)"] = policies_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, contents, headers), "EntryContext": context, } return results def build_blocked_senders_v2_hr_row(policy: dict) -> dict: """Map one flat v2 blocked-senders policy to a war-room table row.""" sender = policy.get("from") or {} receiver = policy.get("to") or {} return { "Policy ID": policy.get("id"), "Sender": sender.get("emailAddress") or sender.get("domain") or (sender.get("group") or {}).get("id"), "Receiver": receiver.get("emailAddress") or receiver.get("domain") or (receiver.get("group") or {}).get("id"), "Bidirectional": policy.get("bidirectional"), "Start": policy.get("fromDateTime"), "End": policy.get("toDateTime"), } def get_blocked_senders_policy_command(policy_id: str) -> CommandResults: """Get a single Blocked Senders policy by ID using the v2 GET API. The flat response is emitted verbatim.""" if not policy_id: raise DemistoException("You need to enter policy ID") policy = http_request("GET", f"{BLOCKED_SENDERS_V2_ENDPOINT}/{policy_id}") return CommandResults( outputs_prefix="Mimecast.BlockedSendersPolicy", outputs=policy, readable_output=tableToMarkdown( "Mimecast Get blockedsenders Policy", build_blocked_senders_v2_hr_row(policy), BLOCKED_SENDERS_HR_HEADERS ), outputs_key_field="id", ) def get_policy_command(args): headers = ["Policy ID", "Sender", "Reciever", "Bidirectional", "Start", "End"] contents = [] policy_id = args.get("policyID") policy_type = args.get("policyType", "blockedsenders") title = f"Mimecast Get {policy_type} Policy" if policy_type == DEFAULT_POLICY_TYPE: return get_blocked_senders_policy_command(policy_id) policies_list = get_policy_request(policy_type, policy_id) policies_context = [] for policy_list in policies_list: policy = policy_list.get("policy", {}) sender = policy.get("from", {}) reciever = policy.get("to", {}) contents.append( { "Policy ID": policy_list["id"], "Sender": { "Group": sender.get("groupId"), "Email Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Reciever": { "Group": reciever.get("groupId"), "Email Address": reciever.get("emailAddress"), "Domain": reciever.get("emailDomain"), "Type": reciever.get("type"), }, "Bidirectional": policy.get("bidirectional"), "Start": policy.get("fromDate"), "End": policy.get("toDate"), } ) policies_context.append( { "ID": policy_list["id"], "Sender": { "Group": sender.get("groupId"), "Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Reciever": { "Group": reciever.get("groupId"), "Address": reciever.get("emailAddress"), "Domain": reciever.get("emailDomain"), "Type": reciever.get("type"), }, "Bidirectional": policy.get("bidirectional"), "FromDate": policy.get("fromDate"), "ToDate": policy.get("toDate"), } ) output_type = { "blockedsenders": "Blockedsenders", "antispoofing-bypass": "AntispoofingBypassPolicy", "address-alteration": "AddressAlterationPolicy", } return [ CommandResults( outputs_prefix="Mimecast.Policy", outputs=policies_context, readable_output=tableToMarkdown(title, contents, headers), outputs_key_field="id", ), CommandResults( outputs_prefix=f"Mimecast.{output_type[policy_type]}", outputs=policies_context, readable_output=tableToMarkdown(title, contents, headers), outputs_key_field="id", ), ] def get_policy_request(policy_type="blockedsenders", policy_id=None): if not policy_type: policy_type = "blockedsenders" api_endpoints = { "blockedsenders": "blockedsenders/get-policy", "antispoofing-bypass": "antispoofing-bypass/get-policy", "address-alteration": "address-alteration/get-address-alteration-set", } api_endpoint = f"/api/policy/{api_endpoints[policy_type]}" data = [] id = "id" if policy_type != "address-alteration" else "folderId" if policy_id: data.append({id: policy_id}) payload = {"data": data} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data") def get_arguments_for_policy_command(args): # type: (dict) -> tuple[dict, str] """ Args: args: Demisto arguments Returns: tuple. policy arguments, and option to choose from the policy configuration. """ spf_domain = args.get("spf_domain") bidirectional = argToBoolean(args.get("bidirectional")) if args.get("bidirectional") else "" comment = args.get("comment", "") enabled = argToBoolean(args.get("enabled")) if args.get("enabled") else "" enforced = argToBoolean(args.get("enforced")) if args.get("enforced") else "" from_date = arg_to_datetime(args.get("from_date")).strftime(DATE_FORMAT) if args.get("from_date") else "" # type: ignore from_eternal = argToBoolean(args.get("from_eternal")) if args.get("from_eternal") else "" to_date = arg_to_datetime(args.get("to_date")).strftime(DATE_FORMAT) if args.get("to_date") else "" # type: ignore to_eternal = argToBoolean(args.get("to_eternal")) if args.get("to_eternal") else "" override = argToBoolean(args.get("override")) if args.get("override") else "" description = args.get("description", "") or args.get("policy_description", "") from_part = args.get("fromPart", "") or args.get("from_part", "") from_type = args.get("fromType", "") or args.get("from_type", "") from_value = args.get("fromValue", "") or args.get("from_value", "") to_type = args.get("toType", "") or args.get("to_type", "") conditions = args.get("conditions") to_value = args.get("toValue", "") or args.get("to_value", "") option = str(args.get("option", "")) policy_obj: dict[str, Any] = { "description": description, "fromType": from_type, "fromValue": from_value, "toType": to_type, "toValue": to_value, "bidirectional": bidirectional, "comment": comment, "enabled": enabled, "enforced": enforced, "override": override, "toDate": to_date, "fromPart": from_part, "fromDate": from_date, "fromEternal": from_eternal, "toEternal": to_eternal, } if spf_domain: policy_obj["conditions"] = {"spfDomains": [spf_domain]} if conditions: policy_obj["conditions"] = {"sourceIPs": [conditions]} return policy_obj, option def create_block_sender_policy_command(policy_args: dict) -> CommandResults: """Create a Blocked Senders policy using the v2 POST API. The response holds only the new policy ID.""" body = build_blocked_senders_policy_v2_body(policy_args) response = http_request("POST", BLOCKED_SENDERS_V2_ENDPOINT, payload=body) policy_id = response.get("id") demisto.debug(f"Created blocked-senders policy {policy_id}") return CommandResults( readable_output=f"Policy {policy_id} was created successfully.", outputs_prefix="Mimecast.BlockedSendersPolicy", outputs_key_field="id", outputs={"id": policy_id}, ) def create_policy_command(): headers = ["Policy ID", "Description", "Sender", "Receiver", "Bidirectional", "Start", "End"] context = {} policy_args = demisto.args() policy_obj, option = get_arguments_for_policy_command(policy_args) policy_list = create_or_update_policy_request(policy_obj, option) policy = policy_list.get("policy") policy_id = policy_list.get("id") title = "Mimecast Create block sender Policy: \n Policy Was Created Successfully!" sender = policy.get("from") receiver = policy.get("to") description = policy.get("description") content = { "Policy ID": policy_id, "Description": description, "Sender": { "Group": sender.get("groupId"), "Email Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Receiver": { "Group": receiver.get("groupId"), "Email Address": receiver.get("emailAddress"), "Domain": receiver.get("emailDomain"), "Type": receiver.get("type"), }, "Reciever": { "Group": receiver.get("groupId"), "Email Address": receiver.get("emailAddress"), "Domain": receiver.get("emailDomain"), "Type": receiver.get("type"), }, "Bidirectional": policy.get("bidirectional"), "Start": policy.get("fromDate"), "End": policy.get("toDate"), } # type: Dict[Any, Any] policies_context = { "ID": policy_id, "Description": description, "Sender": { "Group": sender.get("groupId"), "Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Receiver": { "Group": receiver.get("groupId"), "Address": receiver.get("emailAddress"), "Domain": receiver.get("emailDomain"), "Type": receiver.get("type"), }, "Reciever": { "Group": receiver.get("groupId"), "Email Address": receiver.get("emailAddress"), "Domain": receiver.get("emailDomain"), "Type": receiver.get("type"), }, "Bidirectional": policy.get("bidirectional"), "FromDate": policy.get("fromDate"), "ToDate": policy.get("toDate"), } # type: Dict[Any, Any] context["Mimecast.Policy(val.ID && val.ID == obj.ID)"] = policies_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": policy_list, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, content, headers), "EntryContext": context, } return results def set_empty_value_args_policy_update(policy_obj, option, policy_id): """ The function use the get policy request function to fill the empty arguments in the policy Args: policy_obj (Dict): Dict of policy details option: (str) Policy option policy_id: (str) Policy ID Returns: Tuple. Policy object, the option to configure on the policy, policy id. """ empty_args_list = [] # Add the empty arguments to empty args list for arg, value in policy_obj.items(): if value == "": empty_args_list.append(arg) if option == "": empty_args_list.append("option") # Check if there are any empty arguments if len(empty_args_list) > 0: # Fill the empty arguments with the current data using get policy request function policy_details = get_policy_request(policy_id=policy_id)[0] for arg in empty_args_list: if arg == "option": option = policy_details["option"] else: policy_obj[arg] = policy_details["policy"].get(arg, "") return policy_obj, option, policy_id def update_policy_command(): """ Update policy according to policy ID """ headers = ["Policy ID", "Description", "Sender", "Receiver", "Bidirectional", "Start", "End"] context = {} policy_args = demisto.args() policy_obj, option = get_arguments_for_policy_command(policy_args) policy_id = str(policy_args.get("policy_id", "")) if not policy_id: raise Exception("You need to enter policy ID") policy_obj, option, policy_id = set_empty_value_args_policy_update(policy_obj, option, policy_id) response = create_or_update_policy_request(policy_obj, option, policy_id=policy_id) policy = response.get("policy") title = "Mimecast Update Policy: \n Policy Was Updated Successfully!" sender = policy.get("from") receiver = policy.get("to") description = policy.get("description") contents = { "Policy ID": policy_id, "Description": description, "Sender": { "Group": sender.get("groupId"), "Email Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Receiver": { "Group": receiver.get("groupId"), "Email Address": receiver.get("emailAddress"), "Domain": receiver.get("emailDomain"), "Type": receiver.get("type"), }, "Bidirectional": policy.get("bidirectional"), "Start": policy.get("fromDate"), "End": policy.get("toDate"), } # type: Dict[Any, Any] policies_context = { "ID": policy_id, "Description": description, "Sender": { "Group": sender.get("groupId"), "Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Receiver": { "Group": receiver.get("groupId"), "Address": receiver.get("emailAddress"), "Domain": receiver.get("emailDomain"), "Type": receiver.get("type"), }, "Bidirectional": policy.get("bidirectional"), "FromDate": policy.get("fromDate"), "ToDate": policy.get("toDate"), } # type: Dict[Any, Any] context["Mimecast.Policy(val.ID && val.ID == obj.ID)"] = policies_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, contents, headers), "EntryContext": context, } return results def build_policy_target(target_type: str, value: str | None, attribute_id: str | None, attribute_value: str | None) -> dict: """ Build a v2 policy target object (the 'from' or 'to' member of a blocked-senders policy). The key carrying the value depends on the target type: email_domain uses 'domain', individual_email_address uses 'emailAddress', profile_group uses 'groupId', and address_attribute_value uses a nested 'attribute' object. The remaining types (everyone, internal_addresses, external_addresses) carry no value at all. """ attribute = assign_params(id=attribute_id, value=attribute_value) if target_type == "address_attribute_value" else None return assign_params( type=target_type, domain=value if target_type == "email_domain" else None, emailAddress=value if target_type == "individual_email_address" else None, groupId=value if target_type == "profile_group" else None, attribute=attribute, ) def build_blocked_senders_policy_v2_body(args: dict) -> dict: """Build body for v2 blocked-senders create/update requests.""" from_date_str = arg_to_datetime(args["from_date"]).strftime(DATE_FORMAT) if args.get("from_date") else None # type: ignore to_date_str = arg_to_datetime(args["to_date"]).strftime(DATE_FORMAT) if args.get("to_date") else None # type: ignore body: dict = assign_params( description=args.get("description"), option=args.get("option"), fromPart=args.get("fromPart"), fromDateTime=from_date_str, toDateTime=to_date_str, ) from_type = args.get("fromType") if from_type is not None: body["from"] = build_policy_target( from_type, args.get("fromValue"), args.get("from_attribute_id"), args.get("from_attribute_value"), ) to_type = args.get("toType") if to_type is not None: body["to"] = build_policy_target( to_type, args.get("toValue"), args.get("to_attribute_id"), args.get("to_attribute_value"), ) return body def update_block_sender_policy_command(policy_args: dict) -> CommandResults: """ Update an existing Blocked Senders policy using the v2 PATCH API. Only fields explicitly provided are sent (partial update semantics). """ policy_id = str(policy_args.get("policy_id", "")) if not policy_id: raise DemistoException("You need to enter policy ID") body = build_blocked_senders_policy_v2_body(policy_args) api_endpoint = f"/policy-management/cloud-gateway/v1/blocked-senders/policies/{policy_id}" # is_file=True returns the raw response without parsing JSON, as the endpoint returns 204 No Content http_request("PATCH", api_endpoint, payload=body, is_file=True) return CommandResults(readable_output=f"Policy {policy_id} was updated successfully.") def create_or_update_policy_request(policy, option, policy_id=None, policy_type="blockedsenders"): # Setup required variables # Using dictionary comprehension to filter out keys with None or empty string values policy = {k: v for k, v in policy.items() if v is not None and v != ""} api_endpoint = "/api/policy/blockedsenders/create-policy" payload = {"data": [{"policy": policy, "option": option}]} # Policy ID isnt None if it is an update policy request cause its required to # write a policy ID on update policy command if policy_id: payload["data"][0]["id"] = policy_id response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def delete_policy(args): policy_id = args.get("policyID") policy_type = args.get("policyType") or DEFAULT_POLICY_TYPE if policy_type == DEFAULT_POLICY_TYPE: # is_file=True returns the raw response without parsing JSON, as the endpoint returns 204 No Content http_request("DELETE", f"{BLOCKED_SENDERS_V2_ENDPOINT}/{policy_id}", is_file=True) demisto.debug(f"Deleted blocked-senders policy {policy_id}") else: delete_policy_request(policy_type, policy_id) context = {"ID": policy_id, "Deleted": True} output_type = { "blockedsenders": "BlockedSendersPolicy", "antispoofing-bypass": "AntispoofingBypassPolicy", "address-alteration": "AddressAlterationPolicy", } return [ CommandResults( outputs_prefix="Mimecast.Policy", outputs=context, readable_output=f"Mimecast Policy {policy_id} deleted successfully!", outputs_key_field="ID", ), CommandResults( outputs_prefix=f"Mimecast.{output_type[policy_type]}", outputs=context, readable_output=f"Mimecast Policy {policy_id} deleted successfully!", outputs_key_field="ID", ), ] def delete_policy_request(policy_type, policy_id=None): # Setup required variables api_endpoints = { "antispoofing-bypass": "antispoofing-bypass/delete-policy", "address-alteration": "address-alteration/delete-policy", } api_endpoint = f"/api/policy/{api_endpoints[policy_type]}" id = "id" data = [{id: policy_id}] payload = {"data": data} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) if response.get("data")[0].get("id") != policy_id: raise Exception("Policy was not deleted.") return response.get("data")[0] def manage_sender(): headers = [] # type: List[str] context = {} sender = demisto.args().get("sender") recipient = demisto.args().get("recipient") action = demisto.args().get("action") title_action = "permitted" if action == "permit" else "blocked" title = f"Mimecast messages from {sender} to {recipient} will now be {title_action}!" req_obj = {"sender": sender, "to": recipient, "action": action} managed_sender = manage_sender_request(req_obj) contents = { "Sender": managed_sender.get("sender"), "Recipient": managed_sender.get("to"), "Action": managed_sender.get("type"), "ID": managed_sender.get("id"), } context["Mimecast.Managed(val.ID && val.ID == obj.ID)"] = { "Sender": managed_sender.get("sender"), "Recipient": managed_sender.get("to"), "Action": managed_sender.get("type"), "ID": managed_sender.get("id"), } results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown(title, contents, headers), "EntryContext": context, } return results def manage_sender_request(req_obj): # Setup required variables api_endpoint = "/api/managedsender/permit-or-block-sender" data = [] data.append(req_obj) payload = {"data": data} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def list_managed_url(): headers = ["URL", "Action", "Match Type", "User Awareness", "URL Rewriting", "Comment"] contents = [] context = {} managed_urls_context = [] full_url_response = "" url = demisto.args().get("url") managed_urls = list_managed_url_request() for managed_url in managed_urls: query_string = "" scheme = "" if managed_url.get("queryString"): query_string = "?" + managed_url.get("queryString") if managed_url.get("scheme"): scheme = managed_url.get("scheme") + "://" full_url_response = scheme + managed_url.get("domain", "") + managed_url.get("path", "") + query_string if (url and url in full_url_response) or not url: contents.append( { "URL": full_url_response, "Match Type": managed_url.get("matchType"), "Comment": managed_url.get("comment"), "Action": managed_url.get("action"), "URL Rewriting": managed_url.get("disableRewrite"), "User Awareness": managed_url.get("disableUserAwareness"), } ) managed_urls_context.append( { "Domain": managed_url.get("domain"), "disableLogClick": managed_url.get("disableLogClick"), "Action": managed_url.get("action"), "Path": managed_url.get("path"), "matchType": managed_url.get("matchType"), "ID": managed_url.get("id"), "disableRewrite": managed_url.get("disableRewrite"), } ) context["Mimecast.URL(val.ID && val.ID == obj.ID)"] = managed_urls_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast Managed URLs: ", contents, headers), "EntryContext": context, } return results def list_managed_url_request(): # Setup required variables api_endpoint = "/api/ttp/url/get-all-managed-urls" data = [] # type: List[Any] payload = {"data": data} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data") def create_managed_url(): context = {} contents = {} # type: Dict[Any, Any] managed_urls_context = [] url = demisto.args().get("url") action = demisto.args().get("action") match_type = demisto.args().get("matchType") disable_rewrite = demisto.args().get("disableRewrite") disable_user_awareness = demisto.args().get("disableUserAwareness") disable_log_click = demisto.args().get("disableLogClick") comment = demisto.args().get("comment") url_req_obj = { "comment": comment, "disableRewrite": disable_rewrite, "url": url, "disableUserAwareness": disable_user_awareness, "disableLogClick": disable_log_click, "action": action, "matchType": match_type, } managed_url = create_managed_url_request(url_req_obj) managed_urls_context.append( { "Domain": managed_url.get("domain"), "disableLogClick": managed_url.get("disableLogClick"), "Action": managed_url.get("action"), "Path": managed_url.get("path"), "matchType": managed_url.get("matchType"), "ID": managed_url.get("id"), "disableRewrite": managed_url.get("disableRewrite"), } ) context["Mimecast.URL(val.ID && val.ID == obj.ID)"] = managed_urls_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": f"Managed URL {url} created successfully!", "EntryContext": context, } return results def create_managed_url_request(url_obj): # Setup required variables api_endpoint = "/api/ttp/url/create-managed-url" data = [] data.append(url_obj) payload = {"data": data} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def list_messages(): headers = ["Subject", "Size", "Recieved Date", "From", "Attachment Count", "Message ID"] context = {} contents = [] messages_context = [] search_params = {} limit = arg_to_number(demisto.args().get("limit")) or 20 page = arg_to_number(demisto.args().get("page")) page_size = arg_to_number(demisto.args().get("page_size")) # can't send null values for keys, so if optional value not sent by user, do not add to request. mailbox = demisto.args().get("mailbox", "") if mailbox: search_params["mailbox"] = mailbox view = demisto.args().get("view", "") if view: search_params["view"] = view end_time = demisto.args().get("endTime", "") if end_time: search_params["end"] = end_time start_time = demisto.args().get("startTime", "") if start_time: search_params["start"] = start_time subject = demisto.args().get("subject") messages_list, _ = request_with_pagination( api_endpoint="/api/archive/get-message-list", data=[search_params], limit=limit, page=page, page_size=page_size ) for message in messages_list: if subject == message.get("subject") or not subject: contents.append( { "Message ID": message.get("id"), "Subject": message.get("subject"), "Size": message.get("size"), "Recieved Date": message.get("received"), "From": message.get("from").get("emailAddress"), "Attachment Count": message.get("attachmentCount"), } ) messages_context.append( { "Subject": message.get("subject"), "ID": message.get("id"), "Size": message.get("size"), "RecievedDate": message.get("received"), "From": message.get("from").get("emailAddress"), "AttachmentCount": message.get("attachmentCount"), } ) context["Mimecast.Message(val.ID && val.ID == obj.ID)"] = messages_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast messages list", contents, headers), "EntryContext": context, } return results def get_url_logs(): """ Getting logs using pagination as specified here https://www.mimecast.com/tech-connect/documentation/endpoint-reference/logs-and-statistics/get-ttp-url-logs/ Returns: TTP URl logs command results """ headers = [] # type: List[Any] contents = [] context = {} url_logs_context = [] search_params = {} from_date = demisto.args().get("fromDate", "") to_date = demisto.args().get("toDate", "") scan_result = demisto.args().get("resultType", "") limit = arg_to_number(demisto.args().get("limit")) or 20 page = arg_to_number(demisto.args().get("page")) page_size = arg_to_number(demisto.args().get("page_size")) if from_date: search_params["from"] = from_date if to_date: search_params["to"] = to_date if scan_result: search_params["scanResult"] = scan_result url_logs, _ = request_with_pagination( api_endpoint="/api/ttp/url/get-logs", data=[search_params], response_param="clickLogs", limit=limit, page=page, page_size=page_size, ) for url_log in url_logs: contents.append( { "Action": url_log.get("action"), "Admin Override": url_log.get("adminOverride"), "Category": url_log.get("category"), "Date": url_log.get("date"), "Route": url_log.get("route"), "Scan Result": url_log.get("scanResult"), "URL": url_log.get("url"), "User Awareness Action": url_log.get("userAwarenessAction"), "User Email Address": url_log.get("userEmailAddress"), "User Override": url_log.get("userOverride"), } ) url_logs_context.append( { "Action": url_log.get("action"), "AdminOverride": url_log.get("adminOverride"), "Category": url_log.get("category"), "Date": url_log.get("date"), "Route": url_log.get("route"), "Result": url_log.get("scanResult"), "URL": url_log.get("url"), "Awareness": url_log.get("userAwarenessAction"), "Address": url_log.get("userEmailAddress"), "UserOverride": url_log.get("userOverride"), } ) context["Mimecast.UrlLog"] = url_logs_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast URL logs: ", contents, headers), "EntryContext": context, } return results def get_attachment_logs(): headers = [] # type: List[Any] contents = [] context = {} attachment_logs_context = [] search_params = {} from_date = demisto.args().get("fromDate", "") to_date = demisto.args().get("toDate", "") result = demisto.args().get("resultType", "") limit = arg_to_number(demisto.args().get("limit")) or arg_to_number(demisto.args().get("resultsNumber")) or 20 page = arg_to_number(demisto.args().get("page")) page_size = arg_to_number(demisto.args().get("page_size")) if from_date: search_params["from"] = from_date if to_date: search_params["to"] = to_date if result: search_params["result"] = result attachment_logs, _ = request_with_pagination( api_endpoint="/api/ttp/attachment/get-logs", data=[search_params], response_param="attachmentLogs", limit=limit, page=page, page_size=page_size, ) for attachment_log in attachment_logs: contents.append( { "Result": attachment_log.get("result"), "Date": attachment_log.get("date"), "Sender Address": attachment_log.get("senderAddress"), "File Name": attachment_log.get("fileName"), "Action": attachment_log.get("actionTriggered"), "Route": attachment_log.get("route"), "Details": attachment_log.get("details"), "Recipient Address": attachment_log.get("recipientAddress"), "File Type": attachment_log.get("fileType"), } ) attachment_logs_context.append( { "Result": attachment_log.get("result"), "Date": attachment_log.get("date"), "Sender": attachment_log.get("senderAddress"), "FileName": attachment_log.get("fileName"), "Action": attachment_log.get("actionTriggered"), "Route": attachment_log.get("route"), "Details": attachment_log.get("details"), "Recipient": attachment_log.get("recipientAddress"), "FileType": attachment_log.get("fileType"), } ) context["Mimecast.AttachmentLog"] = attachment_logs_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast attachment logs: ", contents, headers), "EntryContext": context, } return results def get_impersonation_logs(): headers = [] # type: List[Any] contents = [] context = {} impersonation_logs_context = [] search_params = {} from_date = demisto.args().get("fromDate", "") to_date = demisto.args().get("toDate", "") tagged_malicious = demisto.args().get("taggedMalicious", "") search_field = demisto.args().get("searchField", "") query = demisto.args().get("query", "") identifiers = argToList(demisto.args().get("identifiers", "")) actions = argToList(demisto.args().get("actions", "")) limit = arg_to_number(demisto.args().get("limit")) or arg_to_number(demisto.args().get("resultsNumber")) or 20 page = arg_to_number(demisto.args().get("page")) page_size = arg_to_number(demisto.args().get("pageSize")) if from_date: search_params["from"] = from_date if to_date: search_params["to"] = to_date if tagged_malicious: search_params["taggedMalicious"] = tagged_malicious if search_field: search_params["searchField"] = search_field if query: search_params["query"] = query if identifiers: search_params["identifiers"] = identifiers if actions: search_params["actions"] = actions impersonation_logs, result_count = request_with_pagination( api_endpoint="/api/ttp/impersonation/get-logs", data=[search_params], response_param="impersonationLogs", limit=limit, page=page, page_size=page_size, ) for impersonation_log in impersonation_logs: contents.append( { "Result Count": result_count, "Hits": impersonation_log.get("hits"), "Malicious": impersonation_log.get("taggedMalicious"), "Sender IP": impersonation_log.get("senderIpAddress"), "Sender Address": impersonation_log.get("senderAddress"), "Subject": impersonation_log.get("subject"), "Identifiers": impersonation_log.get("identifiers"), "Date": impersonation_log.get("eventTime"), "Action": impersonation_log.get("action"), "Policy": impersonation_log.get("definition"), "ID": impersonation_log.get("id"), "Recipient Address": impersonation_log.get("recipientAddress"), "External": impersonation_log.get("taggedExternal"), } ) impersonation_logs_context.append( { "ResultCount": result_count, "Hits": impersonation_log.get("hits"), "Malicious": impersonation_log.get("taggedMalicious"), "SenderIP": impersonation_log.get("senderIpAddress"), "SenderAddress": impersonation_log.get("senderAddress"), "Subject": impersonation_log.get("subject"), "Identifiers": impersonation_log.get("identifiers"), "Date": impersonation_log.get("eventTime"), "Action": impersonation_log.get("action"), "Policy": impersonation_log.get("definition"), "ID": impersonation_log.get("id"), "RecipientAddress": impersonation_log.get("recipientAddress"), "External": impersonation_log.get("taggedExternal"), } ) context["Mimecast.Impersonation"] = impersonation_logs_context results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast impersonation logs: ", contents, headers), "EntryContext": context, } return results def generate_log_id(log_entry, log_type): """ Generate a unique ID for log entries that don't have a native ID field. Uses the native 'id' field if available, otherwise generates a descriptive ID based on log type. Args: log_entry: The log entry dictionary log_type: Type of log (url, attachment, impersonation, held_message) Returns: A unique identifier string for the log entry """ if log_entry.get("id"): return log_entry.get("id") # Generate descriptive IDs based on log type for better debugging if log_type == "url": # URL logs: use url + date + userEmailAddress return f"{log_entry.get('url')}_{log_entry.get('date')}_{log_entry.get('userEmailAddress', '')}" elif log_type == "attachment": # Attachment logs: use fileName + date + senderAddress + recipientAddress return ( f"{log_entry.get('fileName')}_{log_entry.get('date')}_" f"{log_entry.get('senderAddress')}_{log_entry.get('recipientAddress')}" ) elif log_type == "impersonation": # Impersonation logs: use subject + eventTime + senderAddress return f"{log_entry.get('subject')}_{log_entry.get('date')}_{log_entry.get('senderAddress')}" # Fallback: use MD5 hash of the entire entry for deterministic deduplication return hashlib.md5(json.dumps(log_entry, sort_keys=True).encode()).hexdigest() def get_log_type_fetch_time(last_run: dict, time_key: str, default_fetch_date_time: str) -> str: """ Get the fetch time for a specific log type from last_run, or return default. Args: last_run: The last run context dictionary time_key: The key to look up in last_run (e.g., 'time_url', 'time_attachment') default_fetch_date_time: The default time to use if key not found Returns: Formatted date time string in Mimecast format (YYYY-MM-DDTHH:MM:SS+0000) """ last_fetch = last_run.get(time_key) if last_fetch: fetch_time = datetime.strptime(last_fetch, "%Y-%m-%dT%H:%M:%SZ") return fetch_time.strftime("%Y-%m-%dT%H:%M:%S") + "+0000" return default_fetch_date_time def fetch_incidents(): last_run = demisto.getLastRun() demisto.debug(f"Before fetch {last_run=}") # handle first time fetch - calculate default time for any log type that doesn't have a time yet default_fetch_time = datetime.now() - timedelta(hours=FETCH_DELTA) default_fetch_date_time = default_fetch_time.strftime("%Y-%m-%dT%H:%M:%S") + "+0000" current_fetch = default_fetch_time demisto.debug(f"Default fetch time for first-time fetches: {default_fetch_time=}, {default_fetch_date_time=}") incidents = [] # type: List[Any] new_last_run = {} # type: Dict[str, Any] # Fetch URL logs with enhancement mechanism if FETCH_URL: demisto.debug("Fetching URL logs") url_fetch_date_time = get_log_type_fetch_time(last_run, "time_url", default_fetch_date_time) fetch_log_type( log_type="url", api_endpoint="/api/ttp/url/get-logs", response_param="clickLogs", search_params={"from": url_fetch_date_time, "scanResult": "malicious", "oldestFirst": True}, to_incident_func=url_to_incident, last_run=last_run, current_fetch=current_fetch, incidents=incidents, new_last_run=new_last_run, ) # Fetch Attachment logs with enhancement mechanism if FETCH_ATTACHMENTS: demisto.debug("Fetching Attachment logs") attachment_fetch_date_time = get_log_type_fetch_time(last_run, "time_attachment", default_fetch_date_time) fetch_log_type( log_type="attachment", api_endpoint="/api/ttp/attachment/get-logs", response_param="attachmentLogs", search_params={"from": attachment_fetch_date_time, "scanResult": "malicious", "oldestFirst": True}, to_incident_func=attachment_to_incident, last_run=last_run, current_fetch=current_fetch, incidents=incidents, new_last_run=new_last_run, ) # Fetch Impersonation logs with enhancement mechanism if FETCH_IMPERSONATIONS: demisto.debug("Fetching Impersonation logs") impersonation_fetch_date_time = get_log_type_fetch_time(last_run, "time_impersonation", default_fetch_date_time) fetch_log_type( log_type="impersonation", api_endpoint="/api/ttp/impersonation/get-logs", response_param="impersonationLogs", search_params={"from": impersonation_fetch_date_time, "taggedMalicious": True, "oldestFirst": True}, to_incident_func=impersonation_to_incident, last_run=last_run, current_fetch=current_fetch, incidents=incidents, new_last_run=new_last_run, ) # Fetch Held Messages with enhancement mechanism if FETCH_HELD_MESSAGES: demisto.debug("Fetching Held Messages") held_message_fetch_date_time = get_log_type_fetch_time(last_run, "time_held_message", default_fetch_date_time) fetch_log_type( log_type="held_message", api_endpoint="/api/gateway/get-hold-message-list", response_param=None, search_params={"start": held_message_fetch_date_time, "admin": True}, to_incident_func=held_to_incident, last_run=last_run, current_fetch=current_fetch, incidents=incidents, new_last_run=new_last_run, ) demisto.setLastRun(new_last_run) demisto.debug(f"Changed last_run to {new_last_run=}") demisto.debug(f"saving {len(incidents)} incidents.") demisto.incidents(incidents) def fetch_log_type( log_type, api_endpoint, response_param, search_params, to_incident_func, last_run, current_fetch, incidents, new_last_run, ): """ Generic function to fetch logs of any type with pagination and deduplication. Args: log_type: Type of log (e.g., 'url', 'attachment', 'impersonation', 'held_message') api_endpoint: API endpoint to call response_param: Response parameter containing logs (None for direct data access) search_params: Search parameters for the API call to_incident_func: Function to convert log entry to incident last_run: Last run context from Demisto current_fetch: Current fetch datetime for deduplication incidents: List to append new incidents to new_last_run: Dictionary to update with new last run data """ # Get state from last run time_key = f"time_{log_type}" dedup_key = f"dedup_{log_type}" next_page_key = f"{log_type}_next_page" time_for_next_page_key = f"time_{log_type}_for_next_page" last_fetch_log_type = last_run.get(time_key) dedup_messages = last_run.get(dedup_key, []) current_next_page = last_run.get(next_page_key, "") time_for_next_page = last_run.get(time_for_next_page_key) # Parse last fetch time for this log type if last_fetch_log_type: last_fetch_log_type = datetime.strptime(last_fetch_log_type, "%Y-%m-%dT%H:%M:%SZ") else: last_fetch_log_type = current_fetch current_fetch_log_type = last_fetch_log_type original_query_time = last_fetch_log_type # Handle pagination continuation if time_for_next_page: time_for_next_page = datetime.strptime(time_for_next_page, "%Y-%m-%dT%H:%M:%SZ") current_fetch_log_type = time_for_next_page original_query_time = time_for_next_page demisto.debug(f"{log_type}: {current_next_page=}, {dedup_messages=}, " f"{last_fetch_log_type=}, {current_fetch_log_type=}") # Fetch logs with pagination and deduplication logs, len_of_results, next_page = fetch_logs_with_pagination( api_endpoint=api_endpoint, data=[search_params], response_param=response_param, limit=MAX_FETCH, dedup_messages=dedup_messages, current_next_page=current_next_page, log_type=log_type, ) demisto.debug(f"Fetched {len_of_results} {log_type} logs") # Process each log entry for log_entry in logs: incident = to_incident_func(log_entry) # Generate unique ID for the log entry (uses native ID if available, otherwise generates one) log_id = generate_log_id(log_entry, log_type) temp_date = datetime.strptime(incident["occurred"], "%Y-%m-%dT%H:%M:%SZ") # Update last fetch time and dedup list if temp_date > last_fetch_log_type: demisto.debug(f"{log_type}: Increasing last_fetch since {temp_date=} > {last_fetch_log_type=}") last_fetch_log_type = temp_date dedup_messages = [log_id] if log_id else [] demisto.debug(f"{log_type}: Increased last_fetch to {last_fetch_log_type}") elif temp_date == last_fetch_log_type and log_id: dedup_messages.append(log_id) demisto.debug(f"{log_type}: Appended {log_id} to dedup as temp_date=last_fetch_log_type={last_fetch_log_type}") else: demisto.debug( f"{log_type}: dedup and last_fetch remain the same for {log_id} as {temp_date=} < {last_fetch_log_type=}" ) # Avoid duplication due to weak time query if temp_date >= current_fetch_log_type: incidents.append(incident) else: demisto.debug(f"{log_type}: Did not append {log_id} since {temp_date=} < {current_fetch_log_type=}") demisto.debug(f"{log_type}: Filtered the logs, saving {len(logs)} incidents.") # Update new_last_run with state for this log type if dedup_messages: new_last_run[dedup_key] = dedup_messages demisto.debug(f"{log_type}: Saving {len(dedup_messages)} dedup messages") if next_page: # During pagination, save the next page token and the original query time demisto.debug(f"{log_type}: Pagination active - saving next_page token and original_query_time={original_query_time}") new_last_run[next_page_key] = next_page new_last_run[time_for_next_page_key] = original_query_time.isoformat().split(".")[0] + "Z" demisto.debug(f"{log_type}: NOT updating main time_key during pagination") else: # Pagination complete - update the time with the latest incident time demisto.debug(f"{log_type}: Pagination complete - updating time_key to latest incident time={last_fetch_log_type}") if last_fetch_log_type: new_last_run[time_key] = last_fetch_log_type.isoformat().split(".")[0] + "Z" # Remove pagination keys from last_run as they are no longer relevant demisto.debug(f"{log_type}: Removing pagination keys from last_run") new_last_run.pop(next_page_key, None) new_last_run.pop(time_for_next_page_key, None) demisto.debug( f"{log_type}: Final last_run state - time_key={new_last_run.get(time_key)}, " f"next_page_exists={bool(new_last_run.get(next_page_key))}" ) def url_to_incident(url_log): incident = {} incident["name"] = "Mimecast malicious URL: " + url_log.get("url") incident["occurred"] = url_log.get("date").replace("+0000", "Z") incident["rawJSON"] = json.dumps(url_log) return incident def attachment_to_incident(attachment_log): incident = {} incident["name"] = "Mimecast malicious attachment: " + attachment_log.get("fileName") incident["occurred"] = attachment_log.get("date").replace("+0000", "Z") incident["rawJSON"] = json.dumps(attachment_log) return incident def impersonation_to_incident(impersonation_log): incident = {} incident["name"] = "Mimecast malicious impersonation: " + impersonation_log.get("subject") incident["occurred"] = impersonation_log.get("eventTime").replace("+0000", "Z") incident["rawJSON"] = json.dumps(impersonation_log) incident["dbotMirrorId"] = impersonation_log.get("id") return incident def held_to_incident(held_message): incident = {} incident["name"] = f'Mimecast held message: {held_message.get("subject")}' incident["occurred"] = held_message.get("dateReceived").replace("+0000", "Z") incident["rawJSON"] = json.dumps(held_message) incident["dbotMirrorId"] = held_message.get("id") return incident def discover(): headers = [] # type: List[Any] context = {} context_obj = {} # type: Dict[Any, Any] contents = [] response = discover_request() contents.append( { "Authentication Types": response.get("authenticate"), "Email Address": response.get("emailAddress"), "Email Token": response.get("emailToken"), } ) context_obj = { "AuthenticationTypes": response.get("authenticate"), "EmailAddress": response.get("emailAddress"), "EmailToken": response.get("emailToken"), } context["Mimecast.Authentication(val.EmailAddress && val.EmailAddress === obj.EmailAddress)"] = context_obj results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast Authentication Information", contents, headers), "EntryContext": context, } return results def discover_request(): if not EMAIL_ADDRESS: raise Exception("In order to discover account's auth types, account's email is required.") email = EMAIL_ADDRESS # Setup required variables api_endpoint = "/api/login/discover-authentication" payload = {"data": [{"emailAddress": email}]} response = http_request("POST", api_endpoint, payload, {}, user_auth=False) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def refresh_token(): contents = refresh_token_request() results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": "Token has been refreshed succesfully and is valid for the next 3 days", } return results def refresh_token_request(): if not EMAIL_ADDRESS: raise Exception("In order to refresh a token validty duration, account's email is required.") if not ACCESS_KEY: raise Exception("In order to refresh a token validty duration, account's access key is required.") email = EMAIL_ADDRESS access_key = ACCESS_KEY # Setup required variables api_endpoint = "/api/login/login" payload = {"data": [{"userName": email, "accessKey": access_key}]} response = http_request("POST", api_endpoint, payload, {}, user_auth=False) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def login(): headers = ["Access Key", "Secret Key"] contents = [] response = login_request() contents.append({"Access Key": response.get("accessKey"), "Secret Key": response.get("secretKey")}) results = { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast authentication details \n Tokens are valid for 3 days", contents, headers), } return results def login_request(): if not EMAIL_ADDRESS: raise Exception("In order to refresh a token validty duration, account's email is required.") email = EMAIL_ADDRESS # Setup required variables api_endpoint = "/api/login/login" payload = {"data": [{"userName": email}]} response = http_request("POST", api_endpoint, payload, {}, user_auth=False) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def get_message(): context = {} contents = {} # type: Dict[Any, Any] metadata_context = {} # type: Dict[Any, Any] results = [] message_id = demisto.args().get("messageID") message_context = demisto.args().get("context") message_type = demisto.args().get("type") message_part = demisto.args().get("part") mailbox = demisto.args().get("mailbox") if message_part == "all" or message_part == "metadata": contents, metadata_context = get_message_metadata(message_id) context["Mimecast.Message(val.ID && val.ID === obj.ID)"] = metadata_context results.append( { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": contents, "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Mimecast message details", contents, removeNull=True), "EntryContext": context, } ) if message_part == "all" or message_part == "message": email_file = get_message_body_content_request(message_id, message_context, message_type, mailbox) results.append(fileResult(message_id, email_file)) return results def get_message_body_content_request(message_id, message_context, message_type, mailbox=None): # Setup required variables api_endpoint = "/api/archive/get-message-part" data = [{"id": message_id, "type": message_type, "context": message_context}] # Add mailbox parameter when context is DELIVERED if message_context and message_context.upper() == "DELIVERED": if not mailbox: raise ValueError("The 'mailbox' parameter is required when context is set to 'DELIVERED'") data[0]["mailbox"] = mailbox payload = {"data": data} response = http_request("POST", api_endpoint, payload, is_file=True) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response.content def get_message_metadata(message_id): contents = {} # type: Dict[Any, Any] context = {} # type: Dict[Any, Any] message = get_message_metadata_request(message_id) receivers = message.get("to", []) to_context = [] to_contents = [] for receiver in receivers: to_context.append({"EmailAddress": receiver.get("emailAddress")}) to_contents.append(receiver.get("emailAddress")) copies = message.get("cc", []) cc_context = [] cc_contents = [] for copy in copies: cc_context.append({"EmailAddress": copy.get("emailAddress")}) cc_contents.append(copy.get("emailAddress")) response_headers = message.get("headers", []) headers_contents = [] headers_context = [] for header in response_headers: values = header.get("values") values = list(values) headers_context.append({"Name": header.get("name"), "Values": values}) headers_contents.append("Name: {}, Values: {}".format(str(header.get("name")), str(values))) attachments = message.get("attachments", []) attachments_context = [] attachments_contents = [] for attachment in attachments: attachments_context.append( { "FileName": attachment.get("filename"), "SHA256": attachment.get("sha256"), "ID": attachment.get("id"), "Size": attachment.get("size"), "Extension": attachment.get("extension"), } ) attachments_contents.append( "FileName: {}, SHA256: {}, ID: {}, Size: {}".format( str(attachment.get("filename")), str(attachment.get("sha256")), str(attachment.get("id")), str(attachment.get("size")), ) ) contents = { "Subject": message.get("subject"), "Header Date": message.get("headerDate"), "Size": message.get("size"), "From": message.get("from", {}).get("emailAddress"), "To": to_contents, "Reply To": message.get("replyTo", {}).get("emailAddress"), "CC": cc_contents, "Envelope From": message.get("envelopeFrom", {}).get("emailAddress"), "Headers": headers_contents, "Attachments": attachments_contents, "Processed": message.get("processed"), "Has Html Body": message.get("hasHtmlBody"), "ID": message.get("id"), } context = { "Subject": message.get("subject"), "HeaderDate": message.get("headerDate"), "Size": message.get("size"), "From": message.get("from", {}).get("emailAddress"), "To": to_context, "ReplyTo": message.get("replyTo", {}).get("emailAddress"), "CC": cc_context, "EnvelopeFrom": message.get("envelopeFrom", {}).get("emailAddress"), "Headers": headers_context, "Attachments": attachments_context, "Processed": message.get("processed"), "HasHtmlBody": message.get("hasHtmlBody"), "ID": message.get("id"), } return contents, context def get_message_metadata_request(message_id): # Setup required variables api_endpoint = "/api/archive/get-message-detail" data = [{"id": message_id}] payload = {"data": data} response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return response.get("data")[0] def download_attachment(): attachment_id = demisto.args().get("attachmentID") attachment_name = demisto.args().get("attachmentName") attachment_file = download_attachment_request(attachment_id) return fileResult(attachment_name if attachment_name else attachment_id, attachment_file) def download_attachment_request(attachment_id): # Setup required variables api_endpoint = "/api/archive/get-file" data = [{"id": attachment_id}] payload = {"data": data} response = http_request("POST", api_endpoint, payload, is_file=True) try: json_response = response.json() if json_response.get("fail"): raise Exception(json_response.get("fail", [{}])[0].get("errors")) except ValueError: pass return response.content def find_groups(): api_response = create_find_groups_request() markdown_output = find_groups_api_response_to_markdown(api_response) entry_context = find_groups_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def create_find_groups_request(): api_endpoint = "/api/directory/find-groups" query_string = demisto.args().get("query_string", "") query_source = demisto.args().get("query_source", "") limit = demisto.args().get("limit") meta = {} data = {} if limit: meta["pagination"] = {"pageSize": int(limit)} if query_string: data["query"] = query_string if query_source: data["source"] = query_source payload = {"meta": meta, "data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def find_groups_api_response_to_markdown(api_response): num_groups_found = api_response.get("meta", {}).get("pagination", {}).get("pageSize", 0) query_string = demisto.args().get("query_string", "") query_source = demisto.args().get("query_source", "") if not num_groups_found: md = "### Found 0 groups" if query_string: md += "\n#### query: " + query_string if query_source: md += "\n#### source: " + query_source return md md = "Found " + str(num_groups_found) + " groups:" md_metadata = "" if query_string: md_metadata += "#### query: " + query_string if query_source: if md_metadata: md_metadata += "\n" md_metadata += "#### source: " + query_source groups_list = [] for group in api_response.get("data", [])[0]["folders"]: group_entry = { "Name": group["description"], "Source": group["source"], "Group ID": group["id"], "Number of users": group["userCount"], "Parent ID": group["parentId"], "Number of child groups": group["folderCount"], } groups_list.append(group_entry) md = tableToMarkdown( md, groups_list, ["Name", "Source", "Group ID", "Number of users", "Parent ID", "Number of child groups"], metadata=md_metadata, ) return md def find_groups_api_response_to_context(api_response): groups_list = [] for group in api_response["data"][0]["folders"]: group_entry = { "Name": group["description"], "Source": group["source"], "ID": group["id"], "NumberOfUsers": group["userCount"], "ParentID": group["parentId"], "NumberOfChildGroups": group["folderCount"], } groups_list.append(group_entry) return {"Mimecast.Group(val.ID && val.ID == obj.ID)": groups_list} def get_group_members(): api_response = create_get_group_members_request() markdown_output = group_members_api_response_to_markdown(api_response) entry_context = group_members_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def retrieve_all_results(response, all_results, api_endpoint, meta, data, limit=100): """Retrieve all results according to limit or all_results arguments Args: response (dict): the response of the first request all_results (bool): whether to retrieve all results api_endpoint (str): the api endpoint meta (dict): metadata for request data (dict): data for request limit (int, optional): the limit of group members to retrieve. Defaults to 100. """ next_page = response.get("meta", {}).get("pagination", {}).get("next") group_members = response.get("data", [{}])[0].get("groupMembers", []) while (int(limit) > len(group_members) and next_page) or (all_results and next_page): meta["pagination"] = {"pageToken": next_page} payload = {"meta": meta, "data": [data]} current_response = http_request("POST", api_endpoint, payload) next_page = current_response.get("meta", {}).get("pagination", {}).get("next") current_group_members = current_response.get("data", [{}])[0].get("groupMembers", []) group_members.extend(current_group_members) def create_get_group_members_request(group_id=-1, limit=100): api_endpoint = "/api/directory/get-group-members" args = demisto.args() group_id = args.get("group_id", group_id) limit = args.get("limit", limit) all_results = argToBoolean(args.get("all_results", False)) API_MAX_VALUE = 500 meta = {} data = {} page_size = API_MAX_VALUE if all_results else arg_to_number(limit) meta["pagination"] = {"pageSize": page_size} data["id"] = group_id payload = {"meta": meta, "data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) retrieve_all_results(response, all_results, api_endpoint, meta, data, limit) return response def group_members_api_response_to_markdown(api_response): num_users_found = len(api_response.get("data", [{}])[0].get("groupMembers", [])) group_id = demisto.args().get("group_id", "") if not num_users_found: md = "Found 0 users for group ID: " + group_id + "" return md md = "Found " + str(num_users_found) + " users for group ID: " + group_id users_list = [] for user in api_response["data"][0]["groupMembers"]: user_entry = { "Name": user.get("name"), "Email address": user.get("emailAddress"), "Domain": user.get("domain"), "Type": user.get("type"), "Internal user": user.get("internal"), "Notes": user.get("notes"), } users_list.append(user_entry) md = tableToMarkdown(md, users_list, ["Name", "Email address", "Domain", "Type", "Internal user", "Notes"]) return md def add_users_under_group_in_context_dict(users_list, group_id): demisto_context = demisto.context() if demisto_context and "Mimecast" in demisto_context and "Group" in demisto_context["Mimecast"]: groups_entry_in_context = demisto_context["Mimecast"]["Group"] groups_entry_in_context = ( [groups_entry_in_context] if isinstance(groups_entry_in_context, dict) else groups_entry_in_context ) for group in groups_entry_in_context: if group["ID"] == group_id: group["Users"] = users_list return groups_entry_in_context return [{"ID": group_id, "Users": users_list}] def group_members_api_response_to_context(api_response, group_id=-1): group_id = demisto.args().get("group_id", group_id) users_list = [] for user in api_response["data"][0]["groupMembers"]: user_entry = { "Name": user.get("name"), "EmailAddress": user.get("emailAddress"), "Domain": user.get("domain"), "Type": user.get("type"), "InternalUser": user.get("internal"), "IsRemoved": False, "Notes": user.get("notes"), } users_list.append(user_entry) groups_after_update = add_users_under_group_in_context_dict(users_list, group_id) return {"Mimecast.Group(val.ID && val.ID == obj.ID)": groups_after_update} def add_remove_member_to_group(action_type): """Adds or remove a member from a group Args: action_type: the action type Returns: Demisto Outputs """ if action_type == "add": api_endpoint = "/api/directory/add-group-member" else: api_endpoint = "/api/directory/remove-group-member" api_response = create_add_remove_group_member_request(api_endpoint) markdown_output = add_remove_api_response_to_markdown(api_response, action_type) entry_context = add_remove_api_response_to_context(api_response, action_type) return CommandResults(readable_output=markdown_output, outputs=entry_context, raw_response=api_response) def create_add_remove_group_member_request(api_endpoint): """Adds or remove a member from a group Args: api_endpoint: the add or the remove endpoint Returns: response from API """ group_id = demisto.args().get("group_id", "") email = demisto.args().get("email_address", "") domain = demisto.args().get("domain_address", "") notes = demisto.args().get("notes", "") data = { "id": group_id, } if email: data["emailAddress"] = email if domain: data["domain"] = domain if notes: data["notes"] = notes payload = {"data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def add_remove_api_response_to_markdown(api_response, action_type): """Create a markdown response for the add or remove member operation Args: api_response: response from api action_type: the action type Returns: response from API """ address_modified = api_response["data"][0].get("emailAddress") if not address_modified: address_modified = api_response["data"][0].get("domain", "Address") group_id = api_response["data"][0].get("folderId", "") if action_type == "add": return address_modified + " had been added to group ID " + group_id return address_modified + " has been removed from group ID " + group_id def change_user_status_removed_in_context(user_info, group_id): demisto_context = demisto.context() if demisto_context and "Mimecast" in demisto_context and "Group" in demisto_context["Mimecast"]: groups_entry_in_context = demisto_context["Mimecast"]["Group"] groups_entry_in_context = ( [groups_entry_in_context] if isinstance(groups_entry_in_context, dict) else groups_entry_in_context ) for group in groups_entry_in_context: if group.get("ID") == group_id: for user in group.get("Users", []): if user["EmailAddress"] == user_info.get("EmailAddress", ""): user["IsRemoved"] = True return groups_entry_in_context return [{"ID": group_id, "Users": [user_info]}] def add_remove_api_response_to_context(api_response, action_type): group_id = api_response["data"][0]["folderId"] if action_type == "add": # Run get group members again, to get all relevant data, the response from add user # does not match the get group members. api_response = create_get_group_members_request(group_id=group_id) return group_members_api_response_to_context(api_response, group_id=group_id) else: address_removed = api_response["data"][0].get("emailAddress", "") removed_user = {"EmailAddress": address_removed, "IsRemoved": True} groups_after_update = change_user_status_removed_in_context(removed_user, group_id) return {"Mimecast.Group(val.ID && val.ID == obj.ID)": groups_after_update} def create_group(): api_response = create_group_request() markdown_output = create_group_api_response_to_markdown(api_response) entry_context = create_group_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def create_group_request(): api_endpoint = "/api/directory/create-group" group_name = demisto.args().get("group_name", "") parent_id = demisto.args().get("parent_id", "-1") data = { "description": group_name, } if parent_id != "-1": data["parentId"] = parent_id payload = {"data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def create_group_api_response_to_markdown(api_response): group_name = api_response["data"][0]["description"] group_source = api_response["data"][0]["source"] group_id = api_response["data"][0]["id"] md = group_name + " has been created" group_info = {"Group Source": group_source, "Group ID": group_id} return tableToMarkdown(md, group_info, ["Group Source", "Group ID"]) def create_group_api_response_to_context(api_response): group_created = { "Name": api_response["data"][0]["description"], "Source": api_response["data"][0]["source"], "ID": api_response["data"][0]["id"], "NumberOfUsers": 0, "ParentID": api_response["data"][0]["parentId"], "NumberOfChildGroups": 0, } return {"Mimecast.Group(val.Name && val.Name == obj.Name)": group_created} def update_group(): api_response = create_update_group_request() markdown_output = update_group_api_response_to_markdown(api_response) entry_context = update_group_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def create_update_group_request(): api_endpoint = "/api/directory/update-group" group_name = demisto.args().get("group_name", "") group_id = demisto.args().get("group_id", "") parent_id = demisto.args().get("parent_id", "") data = {"id": group_id} if group_name: data["description"] = group_name if parent_id: data["parentId"] = parent_id payload = {"data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def update_group_api_response_to_markdown(api_response): group_name = api_response["data"][0]["description"] return group_name + " has been updated" def update_group_api_response_to_context(api_response): group_updated = { "ID": api_response["data"][0]["id"], "Name": api_response["data"][0]["description"], "ParentID": api_response["data"][0]["parentId"], } return {"Mimecast.Group(val.ID && val.ID == obj.ID)": group_updated} def create_mimecast_incident(): api_response = create_mimecast_incident_request() markdown_output = mimecast_incident_api_response_to_markdown(api_response, "create") entry_context = mimecast_incident_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def create_mimecast_incident_request(): api_endpoint = "/api/ttp/remediation/create" reason = demisto.args().get("reason", "") start_date = demisto.args().get("start_date", "") end_date = demisto.args().get("end_date", "") search_by = demisto.args().get("search_by", "hash") hash_or_message_id = demisto.args().get("hash_message_id", "") if search_by == "hash": get_hash_type(hash_or_message_id) else: if not hash_or_message_id.startswith("<"): hash_or_message_id = f"<{hash_or_message_id}" if not hash_or_message_id.endswith(">"): hash_or_message_id = f"{hash_or_message_id}>" data = {"reason": reason, "hashOrMessageId": hash_or_message_id, "searchBy": search_by} if start_date: data["start"] = start_date if end_date: data["end"] = end_date payload = {"data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def get_mimecast_incident(): api_response = get_mimecast_incident_request() markdown_output = mimecast_incident_api_response_to_markdown(api_response, "get") entry_context = mimecast_incident_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def get_mimecast_incident_request(): api_endpoint = "/api/ttp/remediation/get-incident" incident_id = demisto.args().get("incident_id", "") data = {"id": incident_id} payload = {"data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def mimecast_incident_api_response_to_markdown(api_response, action_type): response_data = api_response.get("data", [{}])[0] incident_code = response_data.get("code", "") incident_type = response_data.get("type", "") incident_reason = response_data.get("reason", "") incident_identified_messages_amount = response_data.get("identified", 0) incident_successful_messages_amount = response_data.get("successful", 0) incident_failed_messages_amount = response_data.get("failed", 0) incident_restored_messages_amount = response_data.get("restored", 0) incident_id = response_data.get("id", "") if action_type == "create": md = "Incident " + incident_id + " has been created" else: md = "Incident " + incident_id + " has been found" md_metadata = f""" #### Code: {incident_code} #### Type: {incident_type} #### Reason: {incident_reason} #### The number of messages identified based on the search criteria: {incident_identified_messages_amount} #### The number successfully remediated messages: {incident_successful_messages_amount} #### The number of messages that failed to remediate: {incident_failed_messages_amount} #### The number of messages that were restored from the incident: {incident_restored_messages_amount} """ message = response_data["searchCriteria"] message_entry = { "From": message.get("from"), "To": message.get("to"), "Start date": message.get("start"), "End date": message.get("end"), "Message ID": message.get("messageId"), "File hash": message.get("fileHash"), } md = tableToMarkdown( md, message_entry, ["From", "To", "Start", "End date", "Message ID", "File hash"], metadata=md_metadata, removeNull=True ) return md def mimecast_incident_api_response_to_context(api_response): response_data = api_response["data"][0] message = response_data["searchCriteria"] message_entry = { "From": message.get("from"), "To": message.get("to"), "StartDate": message.get("start"), "EndDate": message.get("end"), "MessageID": message.get("messageId"), "FileHash": message.get("fileHash"), } incident_created = { "ID": response_data.get("id"), "Code": response_data.get("code"), "Type": response_data.get("type"), "Reason": response_data.get("reason"), "IdentifiedMessages": response_data.get("identified"), "SuccessfullyRemediatedMessages": response_data.get("successful"), "FailedRemediatedMessages": response_data.get("failed"), "MessagesRestored": response_data.get("restored"), "LastModified": response_data.get("modified"), "SearchCriteria": message_entry, } return {"Mimecast.Incident(val.ID && val.ID == obj.ID)": incident_created} def search_file_hash(): api_response = create_search_file_hash_request() markdown_output = search_file_hash_api_response_to_markdown(api_response) entry_context = search_file_hash_api_response_to_context(api_response) return_outputs(markdown_output, entry_context, api_response) def create_search_file_hash_request(): api_endpoint = "/api/ttp/remediation/search-hash" hashes_to_search = argToList(demisto.args().get("hashes_to_search")) data = {"hashes": hashes_to_search} payload = {"data": [data]} response = http_request("POST", api_endpoint, payload) if isinstance(response, dict) and response.get("fail"): raise Exception(json.dumps(response.get("fail", [{}])[0].get("errors"))) return response def search_file_hash_api_response_to_markdown(api_response): md = "Hashes detected:\n" detected_hashes_list = [] for detected_hash in api_response["data"][0]["hashStatus"]: detected_hash_entry = {"Hash": detected_hash["hash"], "Found within the account": detected_hash["detected"]} detected_hashes_list.append(detected_hash_entry) md = tableToMarkdown(md, detected_hashes_list, ["Hash", "Found within the account"]) md += "### Hashes that failed verification:\n" failed_hash_list = [str(failed_hash) for failed_hash in api_response["data"][0]["failedHashes"]] md += str(failed_hash_list)[1:-1] + "\n" return md def search_file_hash_api_response_to_context(api_response): detected_hashes_list = [] for detected_hash in api_response["data"][0]["hashStatus"]: detected_hash_entry = {"HashValue": detected_hash["hash"], "Detected": detected_hash["detected"]} detected_hashes_list.append(detected_hash_entry) if detected_hashes_list: return {"Mimecast.Hash(val.HashValue && val.HashValue == obj.HashValue)": detected_hashes_list} return None def search_message_command(args): """ Getting message info for specific messages id. Args: args: input arguments for the command. """ response = search_message_request(args) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) tracked_emails = response.get("data")[0].get("trackedEmails") to_transformer = JsonTransformer(func=lambda to_data: ", ".join([to.get("emailAddress", "") for to in to_data])) from_env_transformer = JsonTransformer(func=lambda env: env.get("emailAddress", "")) from_hdr_transformer = JsonTransformer(func=lambda hdr: hdr.get("displayableName", "")) table_json_transformer = {"to": to_transformer, "fromEnv": from_env_transformer, "fromHdr": from_hdr_transformer} headers = { "fromEnv": "From (Envelope)", "fromHdr": "From (Header)", "received": "Date/Time", "senderIP": "IP Address", "spamScore": "Spam Score", "detectionLevel": "Spam Detection", } readable_output = tableToMarkdown( "Tracked Emails", t=tracked_emails, headerTransform=lambda header: headers.get(header) if header in headers else header.capitalize(), removeNull=True, json_transform_mapping=table_json_transformer, ) return CommandResults( outputs_prefix="Mimecast.SearchMessage", outputs_key_field="id", readable_output=readable_output, outputs=tracked_emails, raw_response=response, ) def held_message_summary_command(): """ Getting counts of currently held messages for each hold reason. Args: args: input arguments for the command. """ response = http_request("POST", api_endpoint="/api/gateway/get-hold-summary-list", payload={"data": []}) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) summary_list = response.get("data") headers = {"policyInfo": "Held Reason", "numberOfItems": "Number Of Items"} readable_output = tableToMarkdown( "Message Summary", t=summary_list, headerTransform=lambda header: headers.get(header), removeNull=True ) return CommandResults( outputs_prefix="Mimecast.HeldMessageSummary", outputs_key_field="policyInfo", readable_output=readable_output, outputs=summary_list, raw_response=response, ) def get_message_info_command(args): """ Getting message info for specific messages ids. Args: args: input arguments for the command. """ show_recipient_info = argToBoolean(args.get("show_recipient_info", True)) show_delivered_message = argToBoolean(args.get("show_delivered_message", False)) show_retention_info = argToBoolean(args.get("show_retention_info", True)) show_spam_info = argToBoolean(args.get("show_spam_info", True)) ids = argToList(args.get("ids", "")) results = [] for id in ids: results.append( build_get_message_info_for_specific_id( id, show_recipient_info, show_delivered_message, show_retention_info, show_spam_info ) ) return results def list_held_messages_command(args): """ Getting hold messages list. Args: args: input arguments for the command. """ response, _ = list_held_messages_request(args) from_transformer = JsonTransformer(func=transformer_get_value("emailAddress")) table_json_transformer = {"to": from_transformer, "from": from_transformer, "fromHeader": from_transformer} headers = { "from": "From (Envelope)", "fromHeader": "From (Header)", "policyInfo": "Held Reason", "dateReceived": "Held Since", "hasAttachments": "Has Attachments", "reasonCode": "Reason Code", "reasonId": "reason Id", } readable_output = tableToMarkdown( "Held Messages", t=response, headerTransform=lambda header: headers.get(header) if header in headers else header.capitalize(), removeNull=True, json_transform_mapping=table_json_transformer, headers=[ "id", "dateReceived", "from", "fromHeader", "hasAttachments", "policyInfo", "reason", "reasonCode", "reasonId", "route", "size", "subject", "to", ], metadata=f'Showing page number {args.get("page", "1")}', ) return CommandResults( outputs_prefix="Mimecast.HeldMessage", outputs_key_field="id", readable_output=readable_output, outputs=response, raw_response=response, ) def reject_held_message_command(args): """ Rejecting hold messages. Args: args: input arguments for the command. """ response = reject_held_message_request(args) readable_output = "" if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) for message in response.get("data", []): if not message.get("reject", False): raise Exception(f'Held message with id {message.get("id")} rejection failed.') else: readable_output += f'Held message with id {message.get("id")} was rejected successfully.\n' return CommandResults( readable_output=readable_output, raw_response=response, ) def release_held_message_command(args): """ Rejecting hold messages. Args: args: input arguments for the command. """ id = args.get("id") response = release_held_message_request(id) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) if not response.get("data", [])[0].get("release", False): raise Exception("Message release has failed.") else: readable_output = f"Held message with id {id} was released successfully" return CommandResults( readable_output=readable_output, raw_response=response, ) def search_processing_message_command(args): """ Searching for message being processed. Args: args: input arguments for the command. """ response, _ = search_processing_message_request(args) from_transformer = JsonTransformer(func=transformer_get_value("emailAddress")) table_json_transformer = {"to": from_transformer, "fromHeader": from_transformer, "fromEnv": from_transformer} headers = { "fromEnv": "From (Envelope)", "fromHeader": "From (Header)", "routing": "Route", "created": "Date/Time", "remoteIp": "IP Address", "nextAttempt": "Next Attempt", } readable_output = tableToMarkdown( "Processing Messages", t=response, headerTransform=lambda header: headers.get(header) if header in headers else header.capitalize(), removeNull=True, json_transform_mapping=table_json_transformer, ) return CommandResults( outputs_prefix="Mimecast.ProcessingMessage", readable_output=readable_output, outputs_key_field="id", outputs=response, raw_response=response, ) def list_email_queues_command(args): """ Listing email queue (Inbound and Outbound). Args: args: input arguments for the command. """ response = list_email_queues_request(args) response_data = response.get("data")[0] inbound_data = response_data.get("inboundEmailQueue") outbound_data = response_data.get("outboundEmailQueue") headers = {"date": "Email Queue Date", "count": "Email Queue Count"} total_markdown = tableToMarkdown( "Inbound Email Queue", t=inbound_data, headerTransform=lambda header: f"Inbound {headers.get(header)}" if header in headers else header.capitalize(), removeNull=True, ) total_markdown += tableToMarkdown( "Outbound Email Queue", t=outbound_data, headerTransform=lambda header: f"Outbound {headers.get(header)}" if header in headers else header.capitalize(), removeNull=True, ) return CommandResults(outputs_prefix="Mimecast.EmailQueue", readable_output=total_markdown, outputs=response) def get_archive_search_logs_command(args: dict) -> CommandResults: """ Retrieves archive search logs based on the provided arguments. :param args: A dictionary containing the command arguments. :return: The CommandResults object containing the outputs and raw response. """ api_endpoint = "/api/archive/get-archive-search-logs" query = args.get("query", "") page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) limit = arg_to_number(args.get("limit")) data = assign_params(query=query) result_list = request_with_pagination( api_endpoint, [data], response_param="logs", limit=limit, # type: ignore page=page, page_size=page_size, # type: ignore ) return CommandResults(outputs_prefix="Mimecast.ArchiveSearchLog", outputs=result_list[0]) def get_search_logs_command(args: dict) -> CommandResults: query = args.get("query", "") start = arg_to_datetime(args.get("start")).strftime(DATE_FORMAT) if args.get("start") else None # type: ignore end = arg_to_datetime(args.get("end")).strftime(DATE_FORMAT) if args.get("end") else None # type: ignore page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) limit = arg_to_number(args.get("limit")) data = assign_params(query=query, start=start, end=end) api_endpoint = "/api/archive/get-archive-search-logs" result_list, _ = request_with_pagination( api_endpoint, [data], response_param="logs", limit=limit, # type: ignore page=page, page_size=page_size, # type: ignore ) return CommandResults(outputs_prefix="Mimecast.SearchLog", outputs=result_list[0]) def get_view_logs_command(args: dict) -> CommandResults: query = args.get("query", "") start = arg_to_datetime(args.get("start")).strftime(DATE_FORMAT) if args.get("start") else None # type: ignore end = arg_to_datetime(args.get("end")).strftime(DATE_FORMAT) if args.get("end") else None # type: ignore page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) limit = arg_to_number(args.get("limit")) data = assign_params(query=query, start=start, end=end) response = request_with_pagination( "/api/archive/get-view-logs", [data], limit=limit, # type: ignore page=page, page_size=page_size, # type: ignore ) return CommandResults(outputs_prefix="Mimecast.ViewLog", outputs=response[0]) def list_account_command(args: dict) -> CommandResults: account_name = args.get("account_name", "") account_code = args.get("account_code", "") admin_email = args.get("admin_email", "") region = args.get("region", "") user_count = arg_to_number(args.get("user_count", "")) page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) limit = arg_to_number(args.get("limit")) data = assign_params( accountName=account_name, accountCode=account_code, adminEmail=admin_email, region=region, userCount=user_count ) response = request_with_pagination( "/api/account/get-account", [data], limit=limit, # type: ignore page=page, page_size=page_size, # type: ignore ) return CommandResults(outputs_prefix="Mimecast.Account", outputs=response[0], outputs_key_field="accountCode") def list_blocked_senders_policies_command(args: dict) -> CommandResults: """List Blocked Senders policies using the v2 GET API. The flat items are emitted verbatim. Args: args: Command arguments. Supports ``next_token`` to fetch a specific page returned by a previous call, and ``page_size`` to control the number of results per page. """ params = assign_params( pageToken=args.get("next_token"), pageSize=arg_to_number(args.get("page_size")), ) limit = arg_to_number(args.get("limit")) or 50 response = http_request("GET", BLOCKED_SENDERS_V2_ENDPOINT, params=params) policies = response.get("policies", []) policies = policies[:limit] next_token = (response.get("meta") or {}).get("nextPage") demisto.debug(f"Got {len(policies)} blocked-senders policies, nextPage={next_token!r}") title = "Mimecast list blockedsenders policies: \n These are the existing blockedsenders Policies:" contents = [build_blocked_senders_v2_hr_row(policy) for policy in policies] context_outputs = assign_params(policies=policies, NextToken=next_token) return CommandResults( outputs_prefix="Mimecast.BlockedSendersPolicy", outputs=context_outputs, readable_output=tableToMarkdown(title, contents, BLOCKED_SENDERS_HR_HEADERS), outputs_key_field="id", ) def list_policies_command(args: dict) -> CommandResults: policy_type = args.get("policyType", "blockedsenders") page = arg_to_number(args.get("page")) page_size = arg_to_number(args.get("page_size")) limit = arg_to_number(args.get("limit")) if policy_type == DEFAULT_POLICY_TYPE: return list_blocked_senders_policies_command(args) api_endpoints = { "antispoofing-bypass": "antispoofing-bypass/get-policy", "address-alteration": "address-alteration/get-policy", } api_endpoint = f"/api/policy/{api_endpoints[policy_type]}" policies_list, _ = request_with_pagination( api_endpoint, data=[], limit=limit, # type: ignore page=page, page_size=page_size, ) # type: ignore contents = [] for policy_list in policies_list: policy = policy_list.get("policy", {}) sender = policy.get("from", {}) reciever = policy.get("to", {}) contents.append( { "Policy ID": policy_list["id"], "Sender": { "Group": sender.get("groupId"), "Email Address": sender.get("emailAddress"), "Domain": sender.get("emailDomain"), "Type": sender.get("type"), }, "Reciever": { "Group": reciever.get("groupId"), "Email Address": reciever.get("emailAddress"), "Domain": reciever.get("emailDomain"), "Type": reciever.get("type"), }, "Bidirectional": policy.get("bidirectional"), "Start": policy.get("fromDate"), "End": policy.get("toDate"), } ) headers = ["Policy ID", "Sender", "Reciever", "Bidirectional", "Start", "End"] title = f"Mimecast list {policy_type} policies: \n These are the existing {policy_type} Policies:" output_type = { "antispoofing-bypass": "AntispoofingBypassPolicy", "address-alteration": "AddressAlterationPolicy", } return CommandResults( outputs_prefix=f"Mimecast.{output_type[policy_type]}", outputs=policies_list, readable_output=tableToMarkdown(title, contents, headers), outputs_key_field="id", ) def create_antispoofing_bypass_policy_command(args: dict) -> CommandResults: policy_obj, option = get_arguments_for_policy_command(args) # Using dictionary comprehension to filter out keys with None or empty string values policy_obj = {k: v for k, v in policy_obj.items() if v is not None and v != ""} from_attribute_id = args.get("from_attribute_id") from_attribute_name = args.get("from_attribute_name") from_attribute_value = args.get("from_attribute_value") data: dict[str, Any] = {"option": option, "policy": policy_obj} from_attribute_data = {} if from_attribute_id: from_attribute_data["id"] = from_attribute_id if from_attribute_name: from_attribute_data["name"] = from_attribute_name if from_attribute_value: from_attribute_data["value"] = from_attribute_value if from_attribute_data: data["policy"]["from"] = {"type": "address_attribute_value"} data["policy"]["from"]["attribute"] = from_attribute_data payload = {"data": [data]} api_endpoint = "/api/policy/antispoofing-bypass/create-policy" response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) id = response["data"][0]["id"] return CommandResults( outputs_prefix="Mimecast.AntispoofingBypassPolicy", outputs=response.get("data"), readable_output=f"Anti-Spoofing Bypass policy {id} was created successfully", outputs_key_field="id", ) def update_antispoofing_bypass_policy_command(args: dict) -> CommandResults: description = args.get("description") id = args.get("policy_id") enabled = argToBoolean(args.get("enabled")) from_date = arg_to_datetime(args.get("from_date")).strftime(DATE_FORMAT) if args.get("from_date") else None # type: ignore from_eternal = argToBoolean(args.get("from_eternal")) from_part = args.get("from_part") to_date = arg_to_datetime(args.get("to_date")).strftime(DATE_FORMAT) if args.get("to_date") else None # type: ignore to_eternal = argToBoolean(args.get("to_eternal")) bidirectional = argToBoolean(args.get("bidirectional")) if args.get("bidirectional") else None option = args.get("option") policy = assign_params( description=description, enabled=enabled, fromDate=from_date, fromEternal=from_eternal, fromPart=from_part, toDate=to_date, toEternal=to_eternal, bidirectional=bidirectional, ) data: dict[str, Any] = {"id": id, "option": option, "policy": policy} payload = {"data": [data]} api_endpoint = "/api/policy/antispoofing-bypass/update-policy" response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return CommandResults( outputs_prefix="Mimecast.AntispoofingBypassPolicy", outputs=response.get("data"), readable_output=f"Policy ID- {id} has been updated successfully.", outputs_key_field="id", ) def create_address_alteration_policy_command(args: dict) -> CommandResults: policy_obj, _ = get_arguments_for_policy_command(args) # Using dictionary comprehension to filter out keys with None or empty string values policy_obj = {k: v for k, v in policy_obj.items() if v is not None and v != ""} folder_id = args.get("folder_id") data: dict[str, Any] = {"addressAlterationSetId": folder_id, "policy": policy_obj} payload = {"data": [data]} api_endpoint = "/api/policy/address-alteration/create-policy" response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return CommandResults( outputs_prefix="Mimecast.AddressAlterationPolicy", outputs=response.get("data"), readable_output="Address Alteration policy was created successfully", outputs_key_field="id", ) def update_address_alteration_policy_command(args: dict) -> CommandResults: policy_obj, _ = get_arguments_for_policy_command(args) # Using dictionary comprehension to filter out keys with None or empty string values policy_obj = {k: v for k, v in policy_obj.items() if v is not None and v != ""} id = args.get("policy_id") data: dict[str, Any] = {"id": id, "policy": policy_obj} payload = {"data": [data]} api_endpoint = "/api/policy/address-alteration/update-policy" response = http_request("POST", api_endpoint, payload) if response.get("fail"): raise Exception(json.dumps(response.get("fail")[0].get("errors"))) return CommandResults( outputs_prefix="Mimecast.AddressAlterationPolicy", outputs=response.get("data"), readable_output=f"{id} has been updated successfully", outputs_key_field="id", ) def main(): """COMMANDS MANAGER / SWITCH PANEL""" # Check if token needs to be refresh, if it does and relevant params are set, refresh. command = demisto.command() args = demisto.args() try: if USE_OAUTH2 and any([APP_KEY, APP_ID, SECRET_KEY, ACCESS_KEY]): raise ValueError("When you use API 2.0 (Client ID and Client Secret) do not enter values in api 1.0 fields.") handle_proxy() determine_ssl_usage() if USE_OAUTH2: updating_token_oauth2() elif ACCESS_KEY: auto_refresh_token() if command == "test-module": # This is the call made when pressing the integration test button. test_module() demisto.results("ok") elif command == "fetch-incidents": fetch_incidents() elif command == "mimecast-query": demisto.results(query(args)) elif command == "mimecast-get-policy": return_results(get_policy_command(args)) elif command == "mimecast-list-blocked-sender-policies": return_results(list_blocked_sender_policies_command(args)) elif command == "mimecast-create-policy": demisto.results(create_policy_command()) elif command == "mimecast-create-block-sender-policy": return_results(create_block_sender_policy_command(args)) elif command == "mimecast-update-policy": demisto.results(update_policy_command()) elif command == "mimecast-update-block-sender-policy": return_results(update_block_sender_policy_command(args)) elif command == "mimecast-delete-policy": return_results(delete_policy(args)) elif command == "mimecast-manage-sender": demisto.results(manage_sender()) elif command == "mimecast-list-managed-url": demisto.results(list_managed_url()) elif command == "mimecast-create-managed-url": demisto.results(create_managed_url()) elif command == "mimecast-list-messages": demisto.results(list_messages()) elif command == "mimecast-get-attachment-logs": demisto.results(get_attachment_logs()) elif command == "mimecast-get-url-logs": demisto.results(get_url_logs()) elif command == "mimecast-get-impersonation-logs": demisto.results(get_impersonation_logs()) elif command == "mimecast-url-decode": demisto.results(url_decode()) elif command == "mimecast-discover": demisto.results(discover()) elif command == "mimecast-login": demisto.results(login()) elif command == "mimecast-refresh-token": demisto.results(refresh_token()) elif command == "mimecast-get-message": demisto.results(get_message()) elif command == "mimecast-download-attachments": demisto.results(download_attachment()) elif command == "mimecast-find-groups": find_groups() elif command == "mimecast-get-group-members": get_group_members() elif command == "mimecast-add-group-member": return_results(add_remove_member_to_group("add")) elif command == "mimecast-remove-group-member": return_results(add_remove_member_to_group("remove")) elif command == "mimecast-create-group": create_group() elif command == "mimecast-update-group": update_group() elif command == "mimecast-create-remediation-incident": create_mimecast_incident() elif command == "mimecast-get-remediation-incident": get_mimecast_incident() elif command == "mimecast-search-file-hash": search_file_hash() elif command == "mimecast-search-message": return_results(search_message_command(args)) elif command == "mimecast-held-message-summary": return_results(held_message_summary_command()) elif command == "mimecast-get-message-info": return_results(get_message_info_command(args)) elif command == "mimecast-list-held-message": return_results(list_held_messages_command(args)) elif command == "mimecast-reject-held-message": return_results(reject_held_message_command(args)) elif command == "mimecast-release-held-message": return_results(release_held_message_command(args)) elif command == "mimecast-search-processing-message": return_results(search_processing_message_command(args)) elif command == "mimecast-list-email-queues": return_results(list_email_queues_command(args)) elif command == "mimecast-get-archive-search-logs": return_results(get_archive_search_logs_command(args)) elif command == "mimecast-get-search-logs": return_results(get_search_logs_command(args)) elif command == "mimecast-get-view-logs": return_results(get_view_logs_command(args)) elif command == "mimecast-list-account": return_results(list_account_command(args)) elif command == "mimecast-list-policies": return_results(list_policies_command(args)) elif command == "mimecast-create-antispoofing-bypass-policy": return_results(create_antispoofing_bypass_policy_command(args)) elif command == "mimecast-update-antispoofing-bypass-policy": return_results(update_antispoofing_bypass_policy_command(args)) elif command == "mimecast-create-address-alteration-policy": return_results(create_address_alteration_policy_command(args)) elif command == "mimecast-update-address-alteration-policy": return_results(update_address_alteration_policy_command(args)) except Exception as e: return_error(e) if __name__ in ("__builtin__", "builtins", "__main__"): main()