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.

import json
from datetime import UTC, datetime

import demistomock as demisto  # noqa: F401
import pytest
from CommonServerPython import DemistoException
from freezegun import freeze_time

BASE_URL = "https://api.zoom.us/v2/"
MAX_RECORDS_PER_PAGE = 300


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


@pytest.mark.parametrize(
    "first_fetch_time, expected_result, expect_error",
    [
        ("3 days", "First fetch timestamp: 2023-03-29 00:00:00", False),
        ("6 months", "First fetch timestamp: 2022-10-02 00:00:00", False),
        (
            "7 months",
            "The First fetch time should fall within the last six months."
            " Please provide a valid date within the last six months.",
            True,
        ),
    ],
)
@freeze_time("2023-04-01 00:00:00")
def test_main(first_fetch_time, expected_result, expect_error, mocker):
    """
    Given:
        - 'first_fetch_time': A string representing the first fetch time to be used in the command.
        - 'expected_result': The expected result of the command function.
        - 'expect_error': A boolean indicating whether an error is expected to be raised.
    When:
        - Running the 'main' function.
    Then:
        If 'expect_error' is True: Checks that a 'DemistoException' is raised with a message that
         matches the 'expected_result'.
        If 'expect_error' is False:
            Checks that the output of the command function matches the 'expected_result'.
            Checks that the 'demisto.results' function was called once with the value 'ok'.
            Checks that the 'demisto.info' function was called with the appropriate output.
    """
    from ZoomEventCollector import main

    params = {
        "url": BASE_URL,
        "credentials": {"identifier": "test_id", "password": "test_secret"},
        "account_id": "test_account",
        "first_fetch": first_fetch_time,
    }
    args = {}
    mocker.patch.object(demisto, "params", return_value=params)
    mocker.patch.object(demisto, "args", return_value=args)

    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(demisto, "results")
    mocker.patch("ZoomEventCollector.Client.get_oauth_token", return_value=("token", None))
    mocker.patch(
        "ZoomEventCollector.Client.search_events",
        return_value={"from": "2023-03-31", "to": "2023-04-01", "page_size": 300, "next_page_token": "", "operation_logs": []},
    )
    mocker_info = mocker.patch.object(demisto, "info")
    mock_return_error = mocker.patch("ZoomEventCollector.return_error")

    if expect_error:
        main()
        assert mock_return_error.called
        mock_return_error.assert_called_with(f"Failed to execute test-module command.\nError:\n{expected_result!s}")
    else:
        main()

        output = mocker_info.call_args[0][0]

        assert output == expected_result
        assert demisto.results.call_count == 1
        assert demisto.results.call_args[0][0] == "ok"
        assert mock_return_error.call_count == 0


@freeze_time("2023-03-30 00:00:00")
def test_fetch_events(mocker):
    """
    Tests the fetch_events function

    Given:
        - first_fetch_time
    When:
        - Running the 'fetch_events' function.
    Then:
        - Validates that the function generates the correct API requests with the expected parameters.
        - Validates that the function returns the expected events and next_run timestamps.
    """
    from ZoomEventCollector import Client, fetch_events

    first_fetch_time = datetime(2023, 3, 1).replace(tzinfo=UTC)

    http_request_mocker = mocker.patch.object(
        Client,
        "error_handled_http_request",
        side_effect=[
            util_load_json("test_data/fetch_events_operationlogs.json").get("fetch_events_month_before"),
            util_load_json("test_data/fetch_events_operationlogs.json").get("fetch_events"),
            util_load_json("test_data/fetch_events_activities.json").get("fetch_events_month_before"),
            util_load_json("test_data/fetch_events_activities.json").get("fetch_events"),
        ],
    )

    mocker.patch("ZoomEventCollector.Client.get_oauth_token", return_value=("token", None))
    mocker.patch.object(Client, "generate_oauth_token")

    client = Client(base_url=BASE_URL)
    next_run, events = fetch_events(client, last_run={}, first_fetch_time=datetime(2023, 2, 1).replace(tzinfo=UTC))

    mock_events = util_load_json("test_data/zoom_fetch_events.json")
    assert http_request_mocker.call_args_list[0][1].get("params") == {"page_size": 300, "from": "2023-02-01", "to": "2023-03-01"}
    assert http_request_mocker.call_args_list[1][1].get("params") == {"page_size": 300, "from": "2023-03-02", "to": "2023-03-30"}
    assert http_request_mocker.call_args_list[2][1].get("params") == {"page_size": 300, "from": "2023-02-01", "to": "2023-03-01"}
    assert http_request_mocker.call_args_list[3][1].get("params") == {"page_size": 300, "from": "2023-03-02", "to": "2023-03-30"}
    assert http_request_mocker.call_args_list[0][1].get("url_suffix") == "report/operationlogs"
    assert http_request_mocker.call_args_list[2][1].get("url_suffix") == "report/activities"

    assert events == mock_events
    assert next_run == {"activities": "2023-03-29T11:38:50Z", "operationlogs": "2023-03-21T08:22:09Z"}

    # assert no new results when given the last_run:

    mocker.patch.object(
        Client,
        "error_handled_http_request",
        side_effect=[
            util_load_json("test_data/fetch_events_operationlogs.json").get("fetch_events"),
            util_load_json("test_data/fetch_events_activities.json").get("fetch_events"),
        ],
    )

    next_run, events = fetch_events(
        client,
        last_run={"activities": "2023-03-29T11:38:50Z", "operationlogs": "2023-03-21T08:22:09Z"},
        first_fetch_time=first_fetch_time,
    )
    assert events == []
    assert next_run == {"activities": "2023-03-29T11:38:50Z", "operationlogs": "2023-03-21T08:22:09Z"}


@freeze_time("2023-03-30 00:00:00")
def test_fetch_events_with_last_run(mocker):
    """
    Tests the fetch_events function

    Given:
        - last run object
        - the first request returns next_page_token
        *NOTE* The test simulates a situation where next token is returned, in reality there is no reason why
        next_token would be returned if it is not over 300 results
    When:
        - Running the 'fetch_events' function.
    Then:
        - Ensure the events are returned as expected and the pagination is working as expected
    """
    from ZoomEventCollector import Client, fetch_events

    first_fetch_time = datetime(2023, 3, 1).replace(tzinfo=UTC)

    http_request_mocker = mocker.patch.object(
        Client,
        "error_handled_http_request",
        side_effect=[
            util_load_json("test_data/fetch_events_operationlogs.json").get("fetch_events_with_token"),
            util_load_json("test_data/fetch_events_operationlogs.json").get("fetch_events_with_token_next"),
            util_load_json("test_data/fetch_events_activities.json").get("fetch_events_with_token"),
            util_load_json("test_data/fetch_events_activities.json").get("fetch_events_with_token_next"),
        ],
    )

    mocker.patch("ZoomEventCollector.Client.get_oauth_token", return_value=("token", None))
    mocker.patch.object(Client, "generate_oauth_token")

    client = Client(base_url=BASE_URL)
    next_run, events = fetch_events(
        client,
        last_run={"activities": "2023-03-21T08:14:27Z", "operationlogs": "2023-03-20T16:37:58Z"},
        first_fetch_time=first_fetch_time,
    )

    mock_events = util_load_json("test_data/zoom_fetch_events_with_token.json")
    assert http_request_mocker.call_args_list[0][1].get("params") == {"page_size": 300, "from": "2023-03-20", "to": "2023-03-30"}
    assert http_request_mocker.call_args_list[1][1].get("params") == {
        "page_size": 300,
        "from": "2023-03-20",
        "to": "2023-03-30",
        "next_page_token": "next_token_operationlogs",
    }
    assert http_request_mocker.call_args_list[2][1].get("params") == {"page_size": 300, "from": "2023-03-21", "to": "2023-03-30"}
    assert http_request_mocker.call_args_list[3][1].get("params") == {
        "page_size": 300,
        "from": "2023-03-21",
        "to": "2023-03-30",
        "next_page_token": "next_token_activities",
    }
    assert http_request_mocker.call_args_list[0][1].get("url_suffix") == "report/operationlogs"
    assert http_request_mocker.call_args_list[2][1].get("url_suffix") == "report/activities"

    assert events == mock_events
    assert next_run == {"activities": "2023-03-29T11:38:50Z", "operationlogs": "2023-03-21T08:22:09Z"}


@freeze_time("2023-03-30 00:00:00")
def test_get_events_command(mocker):
    """
    Tests the get-events command function.

        Given:
            - NetBox client and limit of events to fetch
        When:
            - Running the 'get_events' function.
        Then:
            - Checks that the events returned by the 'get_events' function match the expected events.
            - Checks that an exception is raised when the limit exceeds the maximum number of records per page.
    """
    from ZoomEventCollector import Client, get_events

    http_request_mocker = mocker.patch.object(
        Client,
        "error_handled_http_request",
        side_effect=[
            util_load_json("test_data/get_events_operationlogs.json"),
            util_load_json("test_data/get_events_activities.json"),
        ],
    )

    mocker.patch("ZoomEventCollector.Client.get_oauth_token", return_value=("token", None))
    mocker.patch.object(Client, "generate_oauth_token")

    client = Client(base_url=BASE_URL)
    events, results = get_events(client, limit=2, first_fetch_time=datetime(2023, 3, 1).replace(tzinfo=UTC))

    mock_events = util_load_json("test_data/zoom_get_events.json")
    assert http_request_mocker.call_args_list[0][1].get("params") == {"page_size": 2, "from": "2023-03-01", "to": "2023-03-30"}
    assert http_request_mocker.call_args_list[1][1].get("params") == {"page_size": 2, "from": "2023-03-01", "to": "2023-03-30"}
    assert http_request_mocker.call_args_list[0][1].get("url_suffix") == "report/operationlogs"
    assert http_request_mocker.call_args_list[1][1].get("url_suffix") == "report/activities"
    assert events == mock_events

    # Test limit > MAX_RECORDS_PER_PAGE
    with pytest.raises(DemistoException) as e:
        get_events(client, limit=MAX_RECORDS_PER_PAGE + 1, first_fetch_time=datetime(2023, 3, 1).replace(tzinfo=UTC))
    assert (
        e.value.message == f"The requested limit ({MAX_RECORDS_PER_PAGE + 1}) exceeds the maximum number of "
        f"records per page ({MAX_RECORDS_PER_PAGE}). Please reduce the limit and try again."
    )


@pytest.mark.parametrize(
    "input_date, expected_output",
    [
        (datetime(2021, 12, 3), datetime(2022, 1, 3)),
        (datetime(2022, 1, 31), datetime(2022, 2, 28)),
        (datetime(2022, 2, 28), datetime(2022, 3, 28)),
        (datetime(2020, 2, 29), datetime(2020, 3, 29)),
    ],
)
def test_get_next_month(input_date: datetime, expected_output: datetime):
    """
    Given -
        input_date: A datetime object representing the input date.
        expected_output: A datetime object representing the date of the next month on the same day.

    When -
        The function get_next_month is called with the input_date.

    Then -
        Validate that the output of get_next_month is equal to expected_output.
    """
    from ZoomEventCollector import get_next_month

    assert get_next_month(input_date) == expected_output