ZoomEventCollector

This is the Zoom event collector integration for Cortex XSIAM.

Analytics & SIEM · Zoom

Details

IDZoomEventCollector
ProviderZoom
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/auth-utils:1.0.0.10133006
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

This is the Zoom event collector integration for Cortex XSIAM.
This integration was integrated and tested with version 2.0.0 of Zoom

This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Configure Zoom Event Collector in Cortex

Parameter Required
Server URL (e.g., ‘https://api.zoom.us/v2/’) True
Account ID (OAuth) True
Client ID (OAuth) True
Client Secret (OAuth) True
First fetch time (<number> <time unit>, e.g., 12 hours, 7 days) - within the last six months False
Trust any certificate (not secure) False
Use system proxy settings False

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.

zoom-get-events


Gets events from Zoom.

Base Command

zoom-get-events

Input

Argument Name Description Required
should_push_events If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. Required
limit Maximum results to return. The maximum is 300. Optional

Context Output

There is no context output for this command.

Command Example

!zoom-get-events should_push_events=true limit=1

Human Readable Output

operationlogs Events

action category_type operation_detail operator time
Delete User Delete User example@example.com example@example.com 2023-01-16T09:51:59Z

activities Events

client_type email ip_address time type version
Browser example@example.com 8.8.8.8 2023-01-19T14:44:23Z Sign in -

Rate Limits

To prevent abuse and ensure service stability, all API requests are rate limited. Rate limits specify the maximum number of API calls that can be made in a minute period. The exact number of calls that your application can make per minute varies based on company plan.
For more information, please refer to the Zoom API documentation on Rate limits by account type.

Configuration parameters

  • url — Server URL (e.g., 'https://api.zoom.us/v2/') (required)
  • account_id — Account ID (OAuth) (required)
  • credentials — Client ID (OAuth) (required)
  • first_fetch — First fetch time (<number> <time unit>, e.g., 12 hours, 7 days) - within the last six months
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • zoom-get-events

    Gets events from Zoom.

from datetime import UTC, datetime, timedelta
from typing import Any

import demistomock as demisto
import urllib3
from CommonServerPython import *
from dateutil import relativedelta
from ZoomApiModule import *

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

REQUEST_DATE_FORMAT = "%Y-%m-%d"
RESPONSE_TIME_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"

VENDOR = "Zoom"
PRODUCT = "Zoom"


EXTRA_PARAMS = """Too many fields were filled. You should fill the Account ID, Client ID, and Client Secret fields (
OAuth)"""
INVALID_FIRST_FETCH_TIME = "The First fetch time should fall within the last six months."

LOG_TYPES = {"operationlogs": "operation_logs", "activities": "activity_logs"}
EVENT_TYPE = {"operationlogs": "operation_log", "activities": "activity_log"}

""" CLIENT CLASS """


class Client(Zoom_Client):
    """A client class that implements logic to authenticate with Zoom application."""

    def search_events(
        self, log_type: str, first_fetch_time: datetime, last_time: str = "", limit: int = None
    ) -> tuple[str, list[dict[str, Any]]]:
        """
        Searches for Zoom logs using the '/<url_suffix>' API endpoint.
        Args:
            log_type: str, The API endpoint to request.
            last_time: datetime, The datetime of the last event fetched.
            first_fetch_time: datetime, The first fetch time as configured in the integration params.
            limit: int, the limit of the results to return. (is received only in zoom-get-events command)
        Returns:
            Tuple:
                str: The time of the latest event fetched.
                List: A list containing the events.
        """

        results: list[dict] = []
        next_page_token = ""
        next_last_time = last_time

        demisto.debug(f"Last run before the fetch run: {last_time} for {log_type}")
        start_date = first_fetch_time if not last_time else dateparser.parse(last_time).replace(tzinfo=UTC)  # type: ignore[union-attr]
        end_date = datetime.now(UTC)

        demisto.debug(f"Starting to get logs from: {start_date} to: {end_date} for {log_type}")

        while start_date <= end_date:
            first_page = True
            params = {
                "page_size": limit if limit else MAX_RECORDS_PER_PAGE,
                "from": start_date.strftime(REQUEST_DATE_FORMAT),
                "to": get_next_month(start_date).strftime(REQUEST_DATE_FORMAT)
                if start_date.month != end_date.month
                else end_date.strftime(REQUEST_DATE_FORMAT),
            }
            if next_page_token:
                params["next_page_token"] = next_page_token
                first_page = False
            demisto.debug(f"Sending HTTP request to /report/{log_type} with params: {params}")
            response = self.error_handled_http_request(
                method="GET",
                url_suffix=f"report/{log_type}",
                headers={"authorization": f"Bearer {self.access_token}"},
                params=params,
            )

            logs = response.get(LOG_TYPES.get(log_type))
            for i, log in enumerate(logs):
                log_time = log.get("time")
                if not i and first_page:
                    next_last_time = log_time  # save the latest time
                if last_time and last_time == log_time:  # no more results
                    limit = True
                    break
                results.append(log)
            if limit:
                break

            if not (next_page_token := response.get("next_page_token")):
                start_date = get_next_month(start_date) + timedelta(days=1)

        demisto.debug(f"Last run after the fetch run: {next_last_time} for {log_type}")
        return next_last_time, results


def test_module(client: Client) -> str:
    """
    Tests API connectivity and authentication'
    When 'ok' is returned it indicates the integration works like it is supposed to and connection to the service is
    successful.
    Raises exceptions if something goes wrong.
    Args:
        client (Client): Zoom client to use.
    Returns:
        str: 'ok' if test passed, anything else will raise an exception and will fail the test.
    """

    try:
        client.search_events(log_type=next(iter(LOG_TYPES)), limit=1, first_fetch_time=datetime.now(UTC))

    except DemistoException as e:
        error_message = e.message
        if "Invalid access token" in error_message:
            error_message = INVALID_CREDENTIALS
        elif "The Token's Signature resulted invalid" in error_message:
            error_message = INVALID_API_SECRET
        elif "Invalid client_id or client_secret" in error_message:
            error_message = INVALID_ID_OR_SECRET
        else:
            error_message = f"Problem reaching Zoom API, check your credentials. Error message: {error_message}"
        return error_message
    return "ok"


def get_events(
    client: Client, first_fetch_time: datetime, limit: int = MAX_RECORDS_PER_PAGE
) -> tuple[list[dict[str, Any]], CommandResults]:
    """
    Gets all the events from the Zoom API for each log type.
    Args:
        client (Client): Zoom client to use.
        limit: int, the limit of the results to return per log_type.
        first_fetch_time(datetime): If last_run is None (first time we are fetching), it contains the timestamp in
            milliseconds on when to start fetching events.
    Returns:
        list: A list containing the events
        CommandResults: A CommandResults object that contains the events in a table format.
    """

    events: list[dict] = []

    if limit > MAX_RECORDS_PER_PAGE:
        raise DemistoException(
            f"The requested limit ({limit}) exceeds the maximum number of records per page ({MAX_RECORDS_PER_PAGE})."
            f" Please reduce the limit and try again."
        )
    hr = ""
    for log_type in LOG_TYPES:
        _, log_events = client.search_events(log_type=log_type, limit=limit, first_fetch_time=first_fetch_time)
        if log_events:
            hr += tableToMarkdown(name=f"{log_type} Events", t=log_events)
            events.extend(log_events)
        else:
            hr += f"No events found for {log_type}.\n"
    return events, CommandResults(readable_output=hr)


def fetch_events(
    client: Client, last_run: dict[str, str], first_fetch_time: datetime
) -> tuple[dict[str, str], list[dict[str, Any]]]:
    """
    This function retrieves new alerts every interval (default is 1 minute).
    It will use last_run to save the timestamp of the last event it processed.
    If last_run is not provided, it should use the integration parameter first_fetch_time to determine when
    to start fetching the first time.

    Args:
        client (Client): Zoom client to use.
        last_run (dict): A dict with a key containing the latest event time we got from last fetch.
        first_fetch_time(datetime): If last_run is None (first time we are fetching), it contains the timestamp in
            milliseconds on when to start fetching events.
    Returns:
        dict: Next run dictionary containing the timestamp that will be used in ``last_run`` on the next fetch.
        list: List of events that will be created in XSIAM.
    """

    next_run: dict[str, str] = {}
    events = []

    for log_type in LOG_TYPES:
        next_run_time, log_events = client.search_events(
            log_type=log_type,
            last_time=last_run.get(log_type, ""),
            first_fetch_time=first_fetch_time,
        )
        next_run[log_type] = next_run_time
        demisto.debug(f"Received {len(log_events)} events for log type {log_type}")
        log_events = [event | {"event_type": EVENT_TYPE.get(log_type)} for event in log_events]
        events.extend(log_events)

    demisto.debug(f"Returning {len(events)} events in total")
    return next_run, events


def get_next_month(date_obj: datetime) -> datetime:
    """
    Given a datetime object, returns the datetime object of the next month on the same day.

    Args:
        date_obj (datetime): A datetime object representing the input date.

    Returns:
        datetime: A datetime object representing the date of the next month on the same day.

    Examples:
        get_next_month(datetime(2021, 12, 3)) -> 2022-01-03 00:00:00
    """
    return date_obj + relativedelta.relativedelta(months=1)


def call_send_events_to_xsiam(events):
    """Enhances and sends events to XSIAM"""
    for event in events:
        event["_time"] = event.get("time")
    send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)


""" MAIN FUNCTION """


def main() -> None:
    """
    main function, parses params and runs command functions
    """

    params = demisto.params()
    args = demisto.args()

    base_url = params.get("url")
    account_id = params.get("account_id")
    client_id = params.get("credentials", {}).get("identifier")
    client_secret = params.get("credentials", {}).get("password")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    command = demisto.command()

    demisto.debug(f"Command being called is {command}")
    try:
        # How much time before the first fetch to retrieve events
        first_fetch_time = params.get("first_fetch", "3 days")
        first_fetch_datetime: datetime = arg_to_datetime(arg=first_fetch_time, arg_name="First fetch time", required=True)  # type: ignore[assignment]
        if first_fetch_time == "6 months":
            first_fetch_datetime += timedelta(days=1)
        if first_fetch_datetime <= dateparser.parse("6 months", settings={"TIMEZONE": "UTC"}):  # type: ignore[operator]
            raise DemistoException(
                "The First fetch time should fall within the last six months. "
                "Please provide a valid date within the last six months."
            )

        demisto.info(f"First fetch timestamp: {first_fetch_datetime}")

        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            proxy=proxy,
            account_id=account_id,
            client_id=client_id,
            client_secret=client_secret,
        )

        if command == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client)
            return_results(result)

        elif command == "zoom-get-events":
            events, results = get_events(
                client=client,
                limit=arg_to_number(args.get("limit")) or MAX_RECORDS_PER_PAGE,
                first_fetch_time=first_fetch_datetime.replace(tzinfo=UTC),
            )
            return_results(results)

            if argToBoolean(args.pop("should_push_events")):
                call_send_events_to_xsiam(events)

        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            next_run, events = fetch_events(
                client=client,
                last_run=last_run,
                first_fetch_time=first_fetch_datetime.replace(tzinfo=UTC),
            )

            call_send_events_to_xsiam(events)
            # saves next_run for the time fetch-events is invoked
            demisto.debug(f"Set last run to {next_run}")
            demisto.setLastRun(next_run)

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()