Mimecast Event Collector v2
Use the Mimecast Event Collector v2 integration to fetch Audit events and SIEM logs for various SIEM event types, using API 2.0 with OAuth2 authentication.
Email · Mimecast
Details
| ID | Mimecast Event Collector v2 |
|---|---|
| Provider | Permira |
| Category | |
| From Version | 6.8.0 |
| Docker Image | demisto/py3-tools:1.0.0.10120494 |
| Supported Modules | Agentix XSIAM EDR Cortex Cloud Cloud Runtime Security |
README
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM. This integration was developed and tested using Mimecast API 2.0.
Configure Mimecast Event Collector v2 in Cortex
| Parameter | Description | Required |
|---|---|---|
| Base URL | Use the https://api.services.mimecast.com/ Base URL for the Global region. See the the Mimecast guide on API Gateway Options to find the relevant Base URL for other regions. | True |
| Client ID | Refer to the help section for instructions on how to obtain the API 2.0 OAuth2 client credentials. | True |
| Client secret | Refer to the help section for instructions on how to obtain the API 2.0 OAuth2 client credentials. | True |
| Fetch events | False | |
| Fetch event types | Possible values are: Audit, SIEM. | False |
| First fetch timestamp (Audit Events only) | Should be in the <number> <time unit> format (for example, 12 hours, 7 days, 3 months, 1 year). This parameter is only relevant to Audit events. The first fetch timestamp of SIEM logs is internally set to the last minute due to API-side restrictions. | False |
| Maximum number of events per fetch | Default is 1000. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Permissions
Ensure the following permissions when generating OAuth2 credentials for integrating with Mimecast API 2.0:
-
To fetch Audit events, ensure the role assigned to the application is granted the **Account Logs Read** permission. -
To fetch SIEM logs, the logged-in user must be a Mimecast Administrator with the **Security Events and Data Retrieval Threat and Security Events (SIEM) Read** permission or higher.
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
mimecast-get-events
Retrieves Mimecast Audit events and SIEM logs. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.
Base Command
mimecast-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | If True, pushes the events to the Cortex XSIAM dataset. If False, only displays them. Default is False. | Required |
| event_types | The event types to retrieve. Possible values are: Audit, SIEM. | Optional |
| limit | The maximum number of events to retrieve per event type. Default is 10. | Optional |
| start_date | The start date for retrieving events, expressed as relative time (for example, ‘3 hours ago’) or an absolute time in the ISO 8601 format (for example, ‘2025-12-01T00:00:00Z’). Must be within the last 24 hours if retrieving SIEM logs. Default is 1 hour ago. | Optional |
| end_date | The end date for retrieving events, expressed as relative time (for example, ‘2 hours ago’) or an absolute time in the ISO 8601 format (for example, ‘2025-12-02T00:00:00Z’). Must be within the last 24 hours if retrieving SIEM logs. Default is now. | Optional |
Context Output
There is no context output for this command.
Limitations
Due to the data retention period of the Mimecast SIEM CG events endpoint, SIEM logs are only available for fetching within a 24-hour rolling window.
-
If the integration instance is disabled or the Fetch events checkbox is unchecked for a period of more than 24 hours, the event collector will automatically adjust the SIEM collection start time to the most recent available data (within the last 24 hours) upon resumption. This prevents collection failures but may result in a gap in SIEM log coverage during the downtime period.
-
When retrieving SIEM logs using the mimecast-get-events command, ensure both the
start_dateandend_datearguments are within the last 24 hours in the UTC timezone. Values outside this time window will return an error.
Configuration parameters
base_url— Base URL (required)client_credentials— Client ID (required)isFetchEvents— Fetch eventsafter— First fetch timestamp (Audit events only)event_types— Fetch event typesmax_fetch— Maximum number of events per fetchinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
mimecast-get-eventsRetrieves Mimecast Audit events and SIEM logs. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.
import asyncio import aiohttp import hashlib import traceback from enum import Enum from types import DynamicClassAttribute import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 from http import HTTPStatus from typing import cast, Any from collections.abc import Awaitable, Callable from datetime import datetime, timedelta, UTC """ CONSTANTS """ # Date formats AUDIT_DATE_FILTER_FORMAT = "%Y-%m-%dT%H:%M:%S+0000" SIEM_DATE_FILTER_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" # Call the `convert_to_siem_filter_format` function instead of using directly! EVENTS_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # Events dataset VENDOR = "mimecast" PRODUCT = "mimecast" EVENT_TIME_KEY = "_time" SOURCE_LOG_TYPE_KEY = "source_log_type" FILTER_TIME_KEY = "_filter_time" # For last run and deduplication (will be removed before sending to dataset) # Response general error fields IS_FAIL = "fail" IS_ERROR = "error" # Access token response fields and default values ACCESS_TOKEN_KEY = "access_token" TOKEN_TYPE_KEY = "token_type" TOKEN_TTL_KEY = "expires_in" # TTL in seconds TOKEN_VALID_UNTIL_KEY = "valid_until" DEFAULT_TOKEN_TTL = 1800 # 30 minutes DEFAULT_TOKEN_TYPE = "bearer" # Default connection values DEFAULT_BASE_URL = "https://api.services.mimecast.com" # Global region DEFAULT_RETRY_COUNT = 3 # Default limit values DEFAULT_GET_EVENTS_LIMIT = 10 DEFAULT_FETCH_EVENTS_LIMIT = 1000 # Default dates UTC_NOW = datetime.now(tz=UTC) UTC_HOUR_AGO = UTC_NOW - timedelta(hours=1) UTC_MINUTE_AGO = UTC_NOW - timedelta(minutes=1) UTC_WEEK_AGO = UTC_NOW - timedelta(days=7) # Event ID field (for deduplication) EVENT_ID_KEY = "id" # generated if not exists # Last run fields START_DATE_KEY = "start_date" LAST_FETCHED_IDS_KEY = "last_fetched_ids" NEXT_PAGE_KEY = "next_page" class EventTypes(Enum): AUDIT = "audit" SIEM = "siem" @classmethod def all_values(cls) -> list[str]: return [member.value for member in cls] @DynamicClassAttribute def endpoint(self) -> str: mapping = {"audit": "/api/audit/get-audit-events", "siem": "/siem/v1/events/cg"} return mapping[self.value] @DynamicClassAttribute def log_prefix(self) -> str: return "[" + self.value.upper() + "]" @DynamicClassAttribute def source_log_type(self) -> str: mapping = {"audit": "Audit", "siem": "SIEM"} return mapping[self.value] @DynamicClassAttribute def filter_format_func(self) -> Callable[[datetime], str]: mapping = {"audit": convert_to_audit_filter_format, "siem": convert_to_siem_filter_format} return mapping[self.value] @DynamicClassAttribute def event_time_key(self) -> str: mapping = {"audit": "eventTime", "siem": "timestamp"} return mapping[self.value] @DynamicClassAttribute def default_page_size(self) -> int: mapping = {"audit": 500, "siem": 100} return mapping[self.value] DATEPARSER_SETTINGS = {"RETURN_AS_TIMEZONE_AWARE": True, "TIMEZONE": str(UTC)} """ CLIENT CLASS """ class AsyncClient: """An asynchronous client for interacting with the Mimecast API; used for SIEM event collection""" def __init__(self, base_url: str, client_id: str, client_secret: str, verify: bool, proxy: bool): self.base_url = base_url self._client_id = client_id self._client_secret = client_secret self._verify = verify self._proxy_url = handle_proxy().get("http", "") if proxy else None async def __aenter__(self): self._session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=self._verify), proxy=self._proxy_url) return self async def __aexit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: exception_traceback = "".join(traceback.format_exception(exc_type, exc_val, exc_tb)) demisto.error(f"AsyncClient context exited with an exception: {exception_traceback}.") else: demisto.debug("AsyncClient context exited normally.") # Always ensure HTTP client session is closed await self._session.close() async def _generate_new_access_token(self) -> dict[str, Any]: """ Generates a new OAuth2 access token using client credentials flow. Raises: ClientResponseError: If request failed. DemistoException: If response contains an error message and/or no access token. Returns: dict[str, Any]: The token raw API response. """ # Pre-validate to avoid failed API calls due to empty of incomplete OAuth2 client credentials if not (self._client_id and self._client_secret): raise DemistoException( "Empty or incomplete OAuth2 credentials. Specify valid 'Client ID' and 'Client Secret' parameters." ) token_url = urljoin(self.base_url, "/oauth/token") headers = {"Content-Type": "application/x-www-form-urlencoded"} body = { "client_id": self._client_id, "client_secret": self._client_secret, "grant_type": "client_credentials", } demisto.debug(f"Requesting new OAuth2 access token using {token_url=}.") async with self._session.post(url=token_url, headers=headers, data=body) as response: response_json = await response.json() try: response.raise_for_status() except aiohttp.ClientResponseError as e: raise DemistoException( f"Request to {token_url} failed with HTTP {e.status} status. Got response: {response_json}." ) from e if is_fail := response_json.get(IS_FAIL): error_message = is_fail[0]["message"] raise DemistoException(f"Request to {token_url} failed. Got error: {error_message}.") if not response_json.get(ACCESS_TOKEN_KEY): raise DemistoException(f"Request to {token_url} failed. Failed to get access token from response.") token_type = response_json.get(TOKEN_TYPE_KEY) token_ttl = response_json.get(TOKEN_TTL_KEY) # TTL in seconds demisto.debug(f"Successfully obtained OAuth2 access token. {token_type=}, {token_ttl=}.") return response_json async def get_authorization_header(self, force_generate_new_token: bool = False) -> str: """ Constructs Authorization header using the access token in the integration context (if found), or generating a new one. Args: force_generate_new_token (bool, optional): Whether to request a new OAuth access token. Defaults to False. Returns: str: The Authorization header containing the token type and access token. """ demisto.debug(f"Constructing Authorization header using {force_generate_new_token=}.") integration_context = get_integration_context() access_token = integration_context.get(ACCESS_TOKEN_KEY) token_type = integration_context.get(TOKEN_TYPE_KEY, DEFAULT_TOKEN_TYPE) token_valid_until = arg_to_datetime(integration_context.get(TOKEN_VALID_UNTIL_KEY)) is_valid_token = token_valid_until and token_valid_until > UTC_NOW demisto.debug(f"Found in integration context {token_valid_until=}, {is_valid_token=}.") if access_token and is_valid_token and not force_generate_new_token: demisto.debug("Using valid access token in integration context to construct Authorization header.") else: demisto.debug("Generating new access token.") token_response = await self._generate_new_access_token() access_token = token_response.get(ACCESS_TOKEN_KEY) token_type = token_response.get(TOKEN_TYPE_KEY, DEFAULT_TOKEN_TYPE) token_ttl = token_response.get(TOKEN_TTL_KEY, DEFAULT_TOKEN_TTL) token_response[TOKEN_VALID_UNTIL_KEY] = (UTC_NOW + timedelta(seconds=token_ttl)).isoformat() demisto.debug("Saving new access token in integration context.") set_integration_context(token_response) demisto.debug(f"Constructed Authorization header using {token_type=}.") return f"{token_type.capitalize()} {access_token}" async def _handle_request_with_retries( self, method: str, event_type: EventTypes, max_retries: int, **request_kwargs, ) -> dict[str, Any]: """ Handles HTTP requests with automatic retry logic for 401 and 429 errors. Args: method (str): The HTTP method to use (e.g., "GET", "POST"). url (str): The full URL. headers (dict[str, str]): The request headers, including the "Authorization" header. max_retries (int): Maximum number of retries for rate limit errors. context (str): Context string for logging (e.g., event type). **request_kwargs: Additional keyword arguments to pass to the request (e.g., json, params). Raises: ClientResponseError: If the request fails after retries. DemistoException: If the request fails with a non-retryable error. Returns: dict[str, Any]: The API response as JSON. """ log_prefix = event_type.log_prefix url = urljoin(self.base_url, event_type.endpoint) headers = {"Accept": "application/json", "Authorization": await self.get_authorization_header()} retry_count = 0 while retry_count <= max_retries: try: async with self._session.request(method=method, url=url, headers=headers, **request_kwargs) as response: response_json = await response.json() response.raise_for_status() return response_json except aiohttp.ClientResponseError as e: status_code = e.status # Handle 401 Unauthorized - regenerate token and retry once and return if status_code == HTTPStatus.UNAUTHORIZED: demisto.debug(f"{log_prefix} Received HTTP 401 Unauthorized. Generating new token and retrying...") headers["Authorization"] = await self.get_authorization_header(force_generate_new_token=True) # Retry immediately with new token async with self._session.request(method=method, url=url, headers=headers, **request_kwargs) as response: response_json = await response.json() response.raise_for_status() return response_json # Handle 429 Too Many Requests - retry up to `max_retries` with exponential backoff elif status_code == HTTPStatus.TOO_MANY_REQUESTS: if retry_count >= max_retries: raise wait_time = 2**retry_count # Exponential backoff: 1s, 2s, 4s demisto.debug( f"{log_prefix} Received HTTP 429 Too Many Requests. " f"Retrying {retry_count + 1} of {max_retries} after {wait_time} seconds..." ) await asyncio.sleep(wait_time) retry_count += 1 # Handle other errors - don't retry else: demisto.error( f"{log_prefix} {e.request_info.method} Request failed to {e.request_info.url}. " f"Got {status_code=} and {response_json=}." ) raise DemistoException(f"Request to {e.request_info.url} failed with HTTP {e.status} status.") from e # Should not reach here, but just in case raise DemistoException(f"{log_prefix} Failed after {max_retries} retries.") async def get_audit_events( self, start_date: str, end_date: str, next_page: str | None = None, page_size: int = EventTypes.AUDIT.default_page_size, max_retries: int = DEFAULT_RETRY_COUNT, ) -> dict[str, Any]: """ Retrieves audit events from Mimecast. Args: start_date (str): The start date for the audit events in Mimecast format. end_date (str): The end date for the audit events in Mimecast format. next_page (str | None): Optional page token for pagination. page_size (int): The number of items per page. Defaults to 500. max_retries (int): Maximum number of retries for rate limit errors. Defaults to 3. Raises: ClientResponseError: If the request fails (after retry on 401 or 429 errors or some other status code). Returns: dict[str, Any]: A dictionary containing the audit events raw API response. """ log_prefix = EventTypes.AUDIT.log_prefix payload = { "data": [{"startDateTime": start_date, "endDateTime": end_date}], "meta": {"pagination": assign_params(pageSize=page_size, pageToken=next_page)}, } demisto.debug(f"{log_prefix} Requesting events using {payload=}.") response_json = await self._handle_request_with_retries( method="POST", event_type=EventTypes.AUDIT, max_retries=max_retries, json=payload, ) events_count = len(response_json.get("data", [])) demisto.debug(f"{log_prefix} Fetched {events_count} events using {payload=}.") return response_json async def get_siem_events( self, start_date: str | None = None, end_date: str | None = None, next_page: str | None = None, page_size: int = EventTypes.SIEM.default_page_size, max_retries: int = DEFAULT_RETRY_COUNT, ) -> dict[str, Any]: """ Retrieves SIEM logs from Mimecast. Args: start_date (str | None): The start date in ISO 8601 format. next_page (str | None): Optional next page token for pagination. page_size (int): The number of items per page. Defaults to 100. max_retries (int): Maximum number of retries for rate limit errors. Defaults to 3. Raises: ClientResponseError: If the request fails. Returns: dict[str, Any]: A dictionary containing the SIEM logs. """ log_prefix = EventTypes.SIEM.log_prefix if not (start_date or next_page): raise ValueError("Either 'start_date' or 'next_page' should be specified.") params = assign_params( pageSize=page_size, dateRangeStartsAt=start_date, dateRangeEndsAt=end_date, nextPage=next_page, ) demisto.debug(f"{log_prefix} Requesting events using {params=}.") response_json = await self._handle_request_with_retries( method="GET", event_type=EventTypes.SIEM, max_retries=max_retries, params=params, ) events_count = len(response_json.get("value", [])) demisto.debug(f"{log_prefix} Fetched {events_count} events using {params=}.") return response_json """ HELPER FUNCTIONS """ def generate_event_id_if_not_exists(events: list[dict[str, Any]], event_type: EventTypes): """ Generates a unique SHA256 hash as the event ID if the `id` field does not exist in the event JSON. Args: events (list[dict[str, Any]]): The list of events to deduplicate. """ log_prefix = event_type.log_prefix for event in events: if EVENT_ID_KEY in event: continue # SIEM logs do *not* have an "id" field, so we need to generate a unique hash for deduplication event_copy = event.copy() encoded_event: bytes = json.dumps(event_copy, sort_keys=True).encode("utf-8") event_id = str(hashlib.sha256(encoded_event).hexdigest()) event[EVENT_ID_KEY] = event_id demisto.debug(f"{log_prefix} Generated a unique SHA256 {event_id=} using the contents of {event=}.") def convert_to_audit_filter_format(filter_datetime: datetime) -> str: """ Converts datetime object to the `AUDIT_DATE_FILTER_FORMAT` (e.g. 2025-12-24T11:23:41.955Z). Removes last colon from the time format. """ return filter_datetime.strftime(AUDIT_DATE_FILTER_FORMAT) def convert_to_siem_filter_format(filter_datetime: datetime) -> str: """ Converts datetime object to the `SIEM_DATE_FILTER_FORMAT` (e.g. 2025-12-24T11:23:41.955Z). Removes last colon from the time format. """ return filter_datetime.strftime(SIEM_DATE_FILTER_FORMAT)[:-3] + "Z" def is_within_last_24_hours(filter_datetime: datetime | str) -> bool: """ Checks if a given timezone-aware datetime is within the last 24 hours. Args: filter_datetime (datetime | str): A datetime object or a string timestamp. Returns: bool: True if the datetime is within the last 24 hours, False otherwise. """ if isinstance(filter_datetime, str): # Datetime object will be returned since it is set as `required=True` filter_datetime = cast(datetime, arg_to_datetime(filter_datetime, settings=DATEPARSER_SETTINGS, required=True)) # Ensure the input datetime is timezone-aware if filter_datetime.tzinfo is None: demisto.debug(f"No timezone info in provided datetime object. Assuming {str(UTC)} timezone for comparison.") filter_datetime = filter_datetime.replace(tzinfo=timezone.utc) # Calculate the time 24 hours ago twenty_four_hours_ago = UTC_NOW - timedelta(hours=24) # Check if the target datetime is after or equal to the time 24 hours ago return filter_datetime >= twenty_four_hours_ago def deduplicate_and_format_events(events: list[dict], all_fetched_ids: set[str], event_type: EventTypes) -> list[dict]: """ Deduplicates raw events based on a unique identifier and formats them by adding `_time` and `_source_log_type` fields. Args: events (list[dict]): List of raw events. all_fetched_ids (set[str]): Set of event IDs that have already been fetched. event_time_key (str): Key denoting the occurrence time of the each raw event (for deduplication and formatting). event_type (str): Type of event (either "Audit" or "SIEM log"). Returns: list[dict]: A list of deduplicated events. """ log_prefix = event_type.log_prefix demisto.debug(f"{log_prefix} Starting to deduplicate {len(events)} events.") if not events: demisto.debug(f"{log_prefix} No events to deduplicate. Returning empty list.") return [] generate_event_id_if_not_exists(events, event_type) deduplicated_events = [] for event in events: event_id = event[EVENT_ID_KEY] if event_id in all_fetched_ids: demisto.debug(f"{log_prefix} Skipping duplicate {event_id=}.") continue event_time_key = event_type.event_time_key event_time = cast(datetime, arg_to_datetime(event[event_time_key], required=True)) # For dataset event[EVENT_TIME_KEY] = event_time.strftime(EVENTS_DATE_FORMAT) event[SOURCE_LOG_TYPE_KEY] = event_type.source_log_type # For last run (will be removed before sending to dataset) event[FILTER_TIME_KEY] = event_type.filter_format_func(event_time) all_fetched_ids.add(event_id) deduplicated_events.append(event) demisto.debug(f"{log_prefix} Finished deduplicating {len(events)} events. Got {len(deduplicated_events)} unique events.") return deduplicated_events async def get_audit_events( client: AsyncClient, start_date: str, end_date: str, limit: int, last_fetched_ids: list[str] | None = None, ) -> list[dict[str, Any]]: """ Asynchronously fetches audit events from Mimecast for a specific time range. Args: client (AsyncClient): An instance of the AsyncClient. start_date (str): The start time in Mimecast format. end_date (str): The end time in Mimecast format. limit (int): The maximum number of events to retrieve. last_fetched_ids (list[str]): A list of IDs of events that have already been fetched. Returns: list[dict[str, Any]]: List of audit events. """ log_prefix = EventTypes.AUDIT.log_prefix last_fetched_ids = last_fetched_ids or [] all_fetched_ids = set(last_fetched_ids) all_events: list[dict[str, Any]] = [] page_number: int = 0 next_page: str | None = None demisto.debug(f"{log_prefix} Fetching events between {start_date=} and {end_date=}.") while len(all_events) < limit: page_number += 1 page_size = min(EventTypes.AUDIT.default_page_size, (limit - len(all_events))) response = await client.get_audit_events( start_date=start_date, end_date=end_date, next_page=next_page, page_size=page_size, ) # Check for errors (if any) under "fail" key in audit endpoint if errors := response.get(IS_FAIL): raise DemistoException(f"Audit events API call failed with {errors=}.") page_events = response.get("data", []) next_page = response.get("meta", {}).get("pagination", {}).get("next") # Process and deduplicate events deduplicated_events = deduplicate_and_format_events( page_events, all_fetched_ids, event_type=EventTypes.AUDIT, ) all_events.extend(deduplicated_events) # If number of page events is less than requested page size *or* no next page, assume no more events to fetch if len(page_events) < page_size or next_page is None: demisto.debug( f"{log_prefix} No more events available after {page_number=}. " f"Got {len(page_events)} page events and {next_page=}. Breaking..." ) break if len(all_events) >= limit: demisto.debug(f"{log_prefix} Reached {limit=} on {page_number=}. Breaking...") break demisto.debug( f"{log_prefix} Finished fetching {len(all_events)} events from {page_number} pages " f"between {start_date=} and {end_date=}." ) return sorted(all_events, key=lambda item: item[FILTER_TIME_KEY]) async def get_siem_events( client: AsyncClient, start_date: str | None, limit: int, last_fetched_ids: list[str] | None = None, end_date: str | None = None, next_page: str | None = None, ) -> tuple[list[dict[str, Any]], str | None]: """ Asynchronously fetches SIEM logs from Mimecast for a specific SIEM event type. Args: client (AsyncClient): An instance of the AsyncClient. event_type (str): The SIEM event type. start_date (str | None): The start date in ISO 8601 format. limit (int): The maximum number of events to retrieve. last_fetched_ids (list[str]): A list of IDs of events that have already been fetched. next_page (str | None): The last next page token. Returns: tuple[list[dict[str, Any]], str | None]: Tuple of a list of SIEM logs and new next page token. """ log_prefix = EventTypes.SIEM.log_prefix last_fetched_ids = last_fetched_ids or [] all_fetched_ids = set(last_fetched_ids) all_events: list[dict[str, Any]] = [] page_number: int = 0 demisto.debug(f"{log_prefix} Starting to fetch SIEM logs between {start_date=} and {end_date=} with {next_page=}.") while len(all_events) < limit: page_number += 1 page_size = min(EventTypes.SIEM.default_page_size, (limit - len(all_events))) response = await client.get_siem_events( start_date=start_date, end_date=end_date, next_page=next_page, page_size=page_size, ) # Check for errors (if any) under "error" key in SIEM endpoint if errors := response.get(IS_ERROR): raise DemistoException(f"{log_prefix} API call failed with {errors=}.") page_events = response.get("value", []) next_page = response.get("@nextPage") # Process and deduplicate events deduplicated_events = deduplicate_and_format_events( page_events, all_fetched_ids, event_type=EventTypes.SIEM, ) all_events.extend(deduplicated_events) # If number of page events is less than requested page size *or* no next page, assume no more events to fetch if len(page_events) < page_size or next_page is None: demisto.debug( f"{log_prefix} No more events available after {page_number=}. " f"Got {len(page_events)} page events and {next_page=}. Breaking..." ) break if len(all_events) >= limit: demisto.debug(f"{log_prefix} Reached {limit=} on {page_number=}. Breaking...") break demisto.debug( f"{log_prefix} Finished fetching {len(all_events)} events from {page_number} pages " f"between {start_date=} and {end_date=}. Got {next_page=}." ) return sorted(all_events, key=lambda item: item[FILTER_TIME_KEY]), next_page """ COMMAND FUNCTIONS """ async def test_module(client: AsyncClient, event_types: list[str]) -> str: """ Tests the connection to the Mimecast API. Args: client (AsyncClient): An instance of the AsyncClient. event_types (list[str]): List of event types to test. Returns: str: "ok" if connection succeeded. """ demisto.debug(f"Starting test module using {event_types=}.") await fetch_events_command(client, last_run={}, max_fetch=1, event_types=event_types, audit_first_fetch=UTC_MINUTE_AGO) demisto.debug("Test module completed successfully.") return "ok" async def get_events_command(client: AsyncClient, args: dict[str, Any]) -> tuple[list[dict[str, Any]], list[CommandResults]]: """ Implements the `mimecast-get-events` command. Gets events using the AsyncClient. Args: client (AsyncClient): An instance of the AsyncClient. args (dict[str, Any]): The command arguments. Returns: tuple[list[dict[str, Any]], list[CommandResults]]: A tuple of the events list and the CommandResults list. """ limit = arg_to_number(args.get("limit")) or DEFAULT_GET_EVENTS_LIMIT # limit is per event type event_types = argToList(args.get("event_types"), transform=lambda x: x.lower()) or EventTypes.all_values() start_date = arg_to_datetime(args.get("start_date"), settings=DATEPARSER_SETTINGS) or UTC_HOUR_AGO end_date = arg_to_datetime(args.get("end_date"), settings=DATEPARSER_SETTINGS) or UTC_NOW demisto.debug(f"Starting to get events with {limit=} and {event_types=}.") all_events: list[dict[str, Any]] = [] command_results: list[CommandResults] = [] # Fetch audit events if EventTypes.AUDIT.value in event_types: log_prefix = EventTypes.AUDIT.log_prefix audit_start_date = convert_to_audit_filter_format(start_date) audit_end_date = convert_to_audit_filter_format(end_date) demisto.debug(f"{log_prefix} Getting events between {audit_start_date=} and {audit_end_date=} with {limit=}.") audit_events = await get_audit_events(client, start_date=audit_start_date, end_date=audit_end_date, limit=limit) demisto.debug(f"{log_prefix} Got {len(audit_events)} events between {audit_start_date=} and {audit_end_date=}.") all_events.extend(audit_events) human_readable = tableToMarkdown(name="Mimecast Audit Events", t=audit_events) command_results.append(CommandResults(readable_output=human_readable)) # Fetch SIEM logs if EventTypes.SIEM.value in event_types: log_prefix = EventTypes.SIEM.log_prefix if not (is_within_last_24_hours(start_date) and is_within_last_24_hours(end_date)): human_readable = "The 'start_date' and 'end_date' arguments must be within the last 24 hours to get SIEM logs." command_results.append(CommandResults(readable_output=human_readable, entry_type=EntryType.ERROR)) else: siem_start_date = convert_to_siem_filter_format(start_date) siem_end_date = convert_to_siem_filter_format(end_date) demisto.debug(f"{log_prefix} Getting events between {siem_start_date=} and {siem_end_date=} with {limit=}.") siem_events, _ = await get_siem_events(client, start_date=siem_start_date, end_date=siem_end_date, limit=limit) demisto.debug(f"{log_prefix} Got {len(siem_events)} events between {siem_start_date=} and {siem_end_date=}.") all_events.extend(siem_events) human_readable = tableToMarkdown(name="Mimecast SIEM Events", t=siem_events) command_results.append(CommandResults(readable_output=human_readable)) demisto.debug(f"Got {len(all_events)} events in total with {limit=} and {event_types=}.") return all_events, command_results async def fetch_audit_events( client: AsyncClient, audit_last_run: dict, max_fetch: int, first_fetch: datetime, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """ Fetches audit events from Mimecast. Args: client (AsyncClient): An instance of the AsyncClient. audit_last_run (dict): The last run object containing audit event state. max_fetch (int): The maximum number of events to fetch. first_fetch(datetime): The date and time from which to begin to fetch audit events. Returns: tuple[dict[str, Any], list[dict[str, Any]]]: A tuple of (next_run_state, fetched_events). """ log_prefix = EventTypes.AUDIT.log_prefix demisto.debug(f"{log_prefix} Starting to fetch audit events. Got {audit_last_run=}.") start_date = audit_last_run.get(START_DATE_KEY) or convert_to_audit_filter_format(first_fetch) end_date = convert_to_audit_filter_format(UTC_NOW) last_fetched_ids = audit_last_run.get(LAST_FETCHED_IDS_KEY, []) demisto.debug(f"{log_prefix} Fetching events using {start_date=}, {end_date=}, {max_fetch=}.") # Fetch audit events audit_events = await get_audit_events( client, start_date=start_date, end_date=end_date, limit=max_fetch, last_fetched_ids=last_fetched_ids, ) # Update next run state if not audit_events: demisto.debug(f"{log_prefix} No new events found. Keeping {audit_last_run=}.") return audit_last_run, [] new_start_time = audit_events[-1][FILTER_TIME_KEY] new_last_fetched_ids = [event[EVENT_ID_KEY] for event in audit_events if event[FILTER_TIME_KEY] == new_start_time] audit_next_run = {START_DATE_KEY: new_start_time, LAST_FETCHED_IDS_KEY: new_last_fetched_ids} # Remove internal key used for deduplication purposes before sending events to dataset for event in audit_events: event.pop(FILTER_TIME_KEY) demisto.debug(f"{log_prefix} Finished fetching {len(audit_events)} events with {new_start_time=}, {new_last_fetched_ids=}.") return audit_next_run, audit_events async def fetch_siem_events( client: AsyncClient, siem_last_run: dict, max_fetch: int, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """ Fetches SIEM logs from Mimecast for all configured event types concurrently. Args: client (AsyncClient): An instance of the AsyncClient. last_run (dict): The last run object containing SIEM event states for each type. max_fetch (int): The maximum number of events to fetch per event type. Returns: tuple[dict[str, Any], list[dict[str, Any]]]: A tuple of (next_run, all_fetched_events). """ log_prefix = EventTypes.SIEM.log_prefix demisto.debug(f"{log_prefix} Starting to fetch events. Got {siem_last_run=}") default_start_date = convert_to_siem_filter_format(UTC_MINUTE_AGO) last_fetched_ids = siem_last_run.get(LAST_FETCHED_IDS_KEY, []) start_date = siem_last_run.get(START_DATE_KEY) or default_start_date next_page = siem_last_run.get(NEXT_PAGE_KEY) # Ensure the start date within than 24 hours to avoid HTTP 400 (bad request) errors from SIEM API endpoint # If no events were fetched within the last 24 hours, the start date may not be within the allowed API range if not is_within_last_24_hours(start_date): demisto.info(f"{log_prefix} {start_date=} is older than 24 hours. Skipping forward to last 23 hours.") start_date = convert_to_siem_filter_format(UTC_NOW - timedelta(hours=23)) siem_events, new_next_page = await get_siem_events( client, start_date=start_date, limit=max_fetch, last_fetched_ids=last_fetched_ids, next_page=next_page, ) # Handle empty results if not siem_events: demisto.debug(f"{log_prefix} No new events found. Keeping {siem_last_run=}.") return siem_last_run, [] # Update state with newest events new_start_time, new_last_fetched_ids = get_siem_new_start_time_last_fetched_ids(siem_events) siem_next_run = {START_DATE_KEY: new_start_time, LAST_FETCHED_IDS_KEY: new_last_fetched_ids, NEXT_PAGE_KEY: new_next_page} # Remove internal key used for deduplication purposes before sending events to dataset for event in siem_events: event.pop(FILTER_TIME_KEY) demisto.debug( f"{log_prefix} Finished fetching {len(siem_events)} events. " f"Got {new_start_time=}, {new_next_page=}, {new_last_fetched_ids=}." ) return siem_next_run, siem_events def get_siem_new_start_time_last_fetched_ids(events: list[dict[str, Any]]) -> tuple[str, list[str]]: """ Gets the new `start_time` based on the `_filter_time` key in formatted and sorted events as well as the list of last fetched IDs either with the newest time or last page (whichever is larger). Args: events (list[dict[str, Any]]): List of events from the SIEM event type (must be sorted and formatted). event_type (str): SIEM event type name. Returns: tuple[str, str]: Tuple of new start time and list of last fetched IDs. Raises: IndexError: If called with an empty events list. """ log_prefix = EventTypes.SIEM.log_prefix page_size = EventTypes.SIEM.default_page_size new_start_time = events[-1][FILTER_TIME_KEY] events_with_newest_time = [event[EVENT_ID_KEY] for event in events if event.get(FILTER_TIME_KEY) == new_start_time] events_from_last_page = [event[EVENT_ID_KEY] for event in events[-page_size:]] if len(events_with_newest_time) > len(events_from_last_page): demisto.debug(f"{log_prefix} Using event IDs with time={new_start_time} for last run {LAST_FETCHED_IDS_KEY!r} value.") new_last_fetched_ids = events_with_newest_time else: # Sometimes, even when using passing `next_page` to SIEM endpoint, we may get events from previous page on the next page # So save IDs from the last page (default 100) or all IDs with the latest `_filter_time` (whichever is greater) demisto.debug(f"{log_prefix} Using all event IDs in last SIEM page for last run {LAST_FETCHED_IDS_KEY!r} value.") new_last_fetched_ids = events_from_last_page return new_start_time, new_last_fetched_ids async def fetch_events_command( client: AsyncClient, last_run: dict, max_fetch: int, event_types: list[str], audit_first_fetch: datetime, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """ Implements `fetch-events` command. Orchestrates concurrent fetching of both audit events and SIEM logs. Args: client (AsyncClient): An instance of the AsyncClient. last_run (dict): The last run object. max_fetch (int): The maximum number of events to fetch. event_types (list[str]): List of event types to fetch. audit_first_fetch (datetime): The date and time from which to begin to fetch audit events. Returns: tuple[dict[str, Any], list[dict[str, Any]]]: A tuple of next run dictionary and all events list. """ demisto.debug(f"Starting to fetch events with {max_fetch=} and {event_types=}. Got {last_run=}.") run_event_types: list[EventTypes] = [] run_tasks: list[Awaitable[tuple[dict, list]]] = [] # Prepare audit fetch task if EventTypes.AUDIT.value in event_types: audit_type = EventTypes.AUDIT audit_last_run = last_run.get(audit_type.value, {}) run_event_types.append(audit_type) run_tasks.append(fetch_audit_events(client, audit_last_run, max_fetch, audit_first_fetch)) # Prepare SIEM fetch task if EventTypes.SIEM.value in event_types: siem_type = EventTypes.SIEM siem_last_run = last_run.get(siem_type.value, {}) run_event_types.append(siem_type) run_tasks.append(fetch_siem_events(client, siem_last_run, max_fetch)) # Validate run tasks and execute all fetch tasks concurrently if not run_tasks: # If no tasks ran, it means none of the "if" conditions above were not met raise DemistoException(f"Invalid fetch {event_types=}.") demisto.debug(f"Fetching events concurrently for {len(run_tasks)} event types.") results = await asyncio.gather(*run_tasks) next_run: dict[str, Any] = {} all_events: list[dict] = [] # Process results from all run tasks for event_type, event_type_result in zip(run_event_types, results): event_type_next_run, event_type_events = event_type_result next_run[event_type.value] = event_type_next_run all_events.extend(event_type_events) demisto.debug(f"Finished fetching events. Got {len(all_events)} events for {event_types=}. Updating {next_run=}.") return next_run, all_events """ MAIN FUNCTION """ async def main() -> None: # pragma: no cover params: dict[str, Any] = demisto.params() args: dict[str, Any] = demisto.args() command: str = demisto.command() # HTTP Connection base_url: str = params.get("base_url", DEFAULT_BASE_URL).rstrip("/") verify: bool = not params.get("insecure", False) proxy: bool = params.get("proxy", False) # OAuth Credentials client_id: str = params.get("client_credentials", {}).get("identifier", "") client_secret: str = params.get("client_credentials", {}).get("password", "") # Fetch Events audit_first_fetch: datetime = arg_to_datetime(params.get("after")) or UTC_WEEK_AGO max_fetch: int = arg_to_number(params.get("max_fetch")) or DEFAULT_FETCH_EVENTS_LIMIT event_types: list = argToList(params.get("event_types"), transform=lambda x: x.lower()) or EventTypes.all_values() demisto.debug(f"Command being called is {command!r}.") try: async with AsyncClient( base_url=base_url, client_id=client_id, client_secret=client_secret, verify=verify, proxy=proxy, ) as async_client: if command == "test-module": return_results(await test_module(async_client, event_types=event_types)) elif command == "mimecast-get-events": should_push_events = argToBoolean(args.pop("should_push_events", False)) events, command_results = await get_events_command(async_client, args) return_results(command_results) if should_push_events: send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) elif command == "fetch-events": last_run = demisto.getLastRun() next_run, events = await fetch_events_command( async_client, last_run=last_run, max_fetch=max_fetch, event_types=event_types, audit_first_fetch=audit_first_fetch, ) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) demisto.setLastRun(next_run) else: raise NotImplementedError(f"Command {command!r} is not implemented.") except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{str(e)}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): asyncio.run(main())