CiscoWebexEventCollector

Cisco Webex Event Collector fetches Events and Admin Audit Events and Security Audit Events.

Analytics & SIEM · Cisco Webex Teams

Details

IDCiscoWebexEventCollector
ProviderCisco Systems
CategoryAnalytics & SIEM
From Version6.12.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Cisco Webex Event Collector fetches Events and Admin Audit Events and Security Audit Events.
This integration was integrated and tested with version 1 of CiscoWebex API.

Configure Cisco Webex Event Collector on Cortex XSOAR

  1. Navigate to Settings > Automation & Feed Integrations.
  2. Search for Cisco Webex Event Collector.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    URL   True
    Admin APP Client ID Admin APP Client ID and Secret. True
    Admin APP Client Secret   True
    Admin APP Redirect URI   True
    Admin Org Id   True
    Compliance Officer Client ID Compliance Officer Client ID and Secret. True
    Compliance Officer Client Secret   True
    Compliance Officer Redirect URI   True
    Trust any certificate (not secure)   False
    Use system proxy settings   False
    Fetch security audit events To fetch security audit events, the Pro Pack must be installed on the Webex instance, and Allow user authentication data must be enabled. See the note below. False
    Maximum number of events per fetch   False

4.
1. Run the cisco-webex-oauth-start command with the user argument - you will be prompted to sign in to Cisco Webex with your username and password. (make sure you sign in with the same user as you defined in the user argument admin or compliance officer). You will then be redirected to the redirect URI you defined in the application. The URL will contain a query parameter called code. The value of this query parameter will be used in the next command.
2. Run the cisco-webex-oauth-complete command with the user and code arguments The user argument should be set to the same value as in the previous command (admin or compliance officer). The code argument should be set to the value returned in the code query parameter from the previous command.
3. Run the cisco-webex-oauth-test command with the user argument. The user argument should be set to the same value as in the previous command (admin or compliance officer) to ensure connectivity to Cisco Webex.

Note: To fetch security audit events, the Pro Pack must be installed on the Webex instance. Additionally, the Allow user authentication data setting must be enabled:

 Go to Management > Organization Settings.
    In the User authentication data section, toggle Allow user authentication data ON.
    For more details, refer to the [official documentation.](https://help.webex.com/en-us/article/pf66vg/Log-and-analyze-user-sign-ins-and-sign-outs)

Commands

You can execute these commands from the Cortex XSOAR 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.

cisco-webex-oauth-start


Use this command to start the authorization process. In order to authorize the instance, first run the command, and complete the process in the URL that is returned. You will then be redirected to the callback URL where you will copy the authorization code found in the query parameter code, and paste that value in the command !cisco-webex-oauth-complete as an argument to finish the process.

Base Command

cisco-webex-oauth-start

Input

Argument Name Description Required
user The user to start authorization. Possible values are: admin, compliance_officer. Required

Context Output

There is no context output for this command.

cisco-webex-oauth-complete


Use this command to complete the authorization process. After copying the authorization code found in the query parameter code of the callback URL, paste the value in the command as an argument to finish the process.

Base Command

cisco-webex-oauth-complete

Input

Argument Name Description Required
user The user to complete authorization. Possible values are: admin, compliance_officer. Required
code The authorization code retrieved from the callback URL according to the documentation. Required

Context Output

There is no context output for this command.

cisco-webex-oauth-test


Use this command to complete the authorization process. After copying the authorization code found in the query parameter code of the callback URL, paste the value in the command as an argument to finish the process.

Base Command

cisco-webex-oauth-test

Input

Argument Name Description Required
user The user to complete authorization. Possible values are: admin, compliance_officer. Required

Context Output

There is no context output for this command.

cisco-webex-get-admin-audit-events


Gets admin audit events from Cisco Webex.

Base Command

cisco-webex-get-admin-audit-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 number of events to return. Optional
since_datetime Date in ISO format (2023-10-01T20:33:22.123Z) to return events from. Optional

Human Readable Output

Admin Audit Events

_time actorId actorOrgId created data id source_log_type
2023-11-02T09:33:26.408Z 444444 222222 2023-11-02T09:33:26.408Z actorOrgName: panw
targetName: panw
operationType: CREATE
eventDescription: An org setting was created or updated.
actorName: admin@example.com
actorEmail: admin@example.com
settingKey: release_migration
settingName: release_migration
settingValue: “MIGRATED”
trackingId: 111111
previousValue: Null
targetType: ORG
targetId: 222222
actorUserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/2.2.2.2 Safari/537.36
eventCategory: ORG_SETTINGS
actorIp: 1.1.1.1
targetOrgId: 222222
actionText: admin@example.com has modified the value of setting release_migration for ORG “panw”. New value = “MIGRATED”, Previous value = Null.
entityType: ORG
targetOrgName: panw
333333 Admin Audit Events

Context Output

There is no context output for this command.

cisco-webex-get-security-audit-events


Gets security audit events from Cisco Webex.

Base Command

cisco-webex-get-security-audit-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 number of events to return. Optional
since_datetime Date in ISO format (2023-10-01T20:33:22.123Z) to return events from. Optional

Human Readable Output

Security Audit Events

_time actorId actorOrgId created data id source_log_type
2023-12-19T07:01:26.486Z 444444 222222 2023-12-19T07:01:26.486Z actorOrgName: panw
eventDescription: A user attempted logging in
actorName: admin@example.com
actorEmail: admin@example.com
authenticationMethod: Non-Interactive
trackingId: 123456
eventStatus: SUCCESS
actorOauthClient: 111111
actorUserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
eventCategory: LOGINS
actorIp: 1.1.1.1
actorClientName: Developer Portal
actionText: admin@example.com attempted logging into panw using client (Developer Portal) and Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36. Login status: SUCCESS.
failedReason:
333333 Security Audit Events

Context Output

There is no context output for this command.

cisco-webex-get-compliance-officer-events


Gets events from Cisco Webex.

Base Command

cisco-webex-get-compliance-officer-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 number of events to return. Optional
since_datetime Date in ISO format (2023-10-01T20:33:22.123Z) to return events from. Optional

Human Readable Output

Events

_time actorId created data id resource source_log_type type
2023-11-05T13:33:46.417Z 222222 2023-11-05T13:33:46.417Z id: 333333
roomId: 444444
roomType: group
personId: 222222
personEmail: jsmith@example.com
personDisplayName: Kfir Testman
personOrgId: 555555
isModerator: false
isMonitor: false
isRoomHidden: false
created: 2023-11-05T13:33:46.417Z
111111 memberships Events created

Context Output

There is no context output for this command.

Configuration parameters

  • base_url — URL (required)
  • admin_credentials — Admin APP Client ID (required)
  • admin_app_redirect_uri — Admin APP Redirect URI (required)
  • admin_org_id — Admin Org Id (required)
  • compliance_officer_credentials — Compliance Officer Client ID (required)
  • compliance_officer_redirect_uri — Compliance Officer Redirect URI (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • fetch_security_audit_events — Fetch security audit events
  • max_fetch — Maximum number of events per fetch

Commands (6)

  • cisco-webex-get-admin-audit-events

    Gets admin audit events from Cisco Webex.

  • cisco-webex-get-compliance-officer-events

    Gets events from Cisco Webex.

  • cisco-webex-get-security-audit-events

    Gets security audit events from Cisco Webex.

  • cisco-webex-oauth-complete

    Use this command to complete the authorization process. After copying the authorization code found in the query parameter `code` of the callback URL, paste the value in the command as an argument to finish the process.

  • cisco-webex-oauth-start

    Use this command to start the authorization process. In order to authorize the instance, first run the command, and complete the process in the URL that is returned. You will then be redirected to the callback URL where you will copy the authorization code found in the query parameter `code`, and paste that value in the command `!cisco-webex-oauth-complete` as an argument to finish the process.

  • cisco-webex-oauth-test

    Use this command to complete the authorization process. After copying the authorization code found in the query parameter `code` of the callback URL, paste the value in the command as an argument to finish the process.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
from CommonServerUserPython import *  # noqa
from dateutil import parser
from collections.abc import Callable

""" CONSTANTS """

VENDOR = "cisco"
PRODUCT = "webex"
SCOPE = {
    "admin": "audit:events_read spark:kms",
    "compliance_officer": "spark-compliance:events_read spark:kms",
}
COMMAND_FUNCTION_TO_EVENT_TYPE = {
    "get_admin_audits": "Admin Audit Events",
    "get_security_audits": "Security Audit Events",
    "get_compliance_officer_events": "Events",
}
DEFAULT_MAX_FETCH = 200

""" HELPER FUNCTIONS """


def remove_integration_context_for_user(user: str):
    """
    Remove integration context for a user.
    Used when running the oath-start command to avoid using the deprecated access token saved in the context data.
    Args:
        user: The user to remove the integration context for.
    """
    integration_context = get_integration_context()
    integration_context[user] = {}
    set_integration_context(integration_context)


def date_time_to_iso_format(date_time: datetime) -> str:
    """
    Gets a datetime object and returns s string represents a datetime is ISO format.
    Args:
        date_time: A datetime object.

    Returns:
        A string represents a datetime is ISO format.
    """
    return f'{date_time.isoformat(timespec="milliseconds")}Z'


def create_last_run() -> dict:
    """
    Creates a dict fetch data (last event datetime, next link) for each event type.
    Returns:
        A dict used in the fetch events command for the first fetch.
    """
    start_fetch = datetime.utcnow() - timedelta(weeks=1)
    return {
        "admin_audits": {"since_datetime": date_time_to_iso_format(start_fetch), "next_url": ""},
        "security_audits": {"since_datetime": date_time_to_iso_format(start_fetch), "next_url": ""},
        "compliance_officer_events": {"since_datetime": date_time_to_iso_format(start_fetch), "next_url": ""},
    }


def add_fields_to_events(events: list[dict], evnet_type: str | None):
    """
    Gets a list of events of a specific event type and adds the `_time` & `source_log_type` fields to the event.
    Args:
        events: A list of events.
        evnet_type: The event type.
    """
    for event in events:
        event["_time"] = event.get("created")
        event["source_log_type"] = evnet_type


def increase_datetime_for_next_fetch(events: list, latest_datetime_previous_fetch: str) -> str:
    """
    Gets a list of events and a string represents a datetime from the previous fetch
    and returns the latest event create time + a timedelta of a millisecond using for the next fetch.
    Args:
        events: A list of events.
        latest_datetime_previous_fetch: A string represents a datetime in ISO format saved from the previous fetch.

    Returns:
        A string represents a datetime is ISO format.
    """
    latest_event = max(events, key=lambda event: parser.parse(event["created"]))
    latest_date_time = max(
        parser.parse(latest_event.get("created"), ignoretz=True), parser.parse(latest_datetime_previous_fetch, ignoretz=True)
    )
    return date_time_to_iso_format(latest_date_time + timedelta(milliseconds=1))


""" CLIENT CLASS """


class Client(BaseClient):
    """Client class to interact with the service API"""

    def __init__(
        self,
        url: str,
        verify: bool,
        proxy: bool,
        client_id: str,
        client_secret: str,
        redirect_uri: str,
        scope: str | None,
        user: str,
    ):
        super().__init__(base_url=url, verify=verify, proxy=proxy)
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri
        self.scope = scope
        self.user = user

    def create_access_token(self, grant_type: str, code: str | None = None, refresh_token: str | None = None) -> dict:
        """
        Generates a refresh & and access tokens.
        Args:
            grant_type: the grant_type could be either `authorization_code` or `refresh_token`.
            refresh_token: the `refresh_token` to generate the `access_token` when expires.
            code: string returns as a query parameter from the `!cisco-webex-oauth-start` command.
        """
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        params = assign_params(
            grant_type=grant_type,
            code=code,
            refresh_token=refresh_token,
            client_id=self.client_id,
            client_secret=self.client_secret,
            redirect_uri=self.redirect_uri,
        )
        return self._http_request(method="POST", url_suffix="access_token", headers=headers, data=params)

    def save_tokens_to_integration_context(self, result: dict):
        """
        Saves the access & refresh tokens in the integration context.
        Args:
            result: The API json response from the `create_access_token` method.
        """
        now = datetime.utcnow()
        context = assign_params(
            access_token=result.get("access_token"),
            access_token_expires_in=date_time_to_iso_format(now + timedelta(seconds=result.get("expires_in", 0))),
            refresh_token=result.get("refresh_token"),
            refresh_token_expires_in=date_time_to_iso_format(now + timedelta(seconds=result.get("refresh_token_expires_in", 0))),
        )
        integration_context = get_integration_context()
        integration_context[self.user] = context
        set_integration_context(integration_context)

    def get_access_token(self) -> str | None:
        """
        Returns the access token from the integration context or generates a new one using the refresh_token.
        Returns:
            The access token or None if the integration context is not set yet.
        """
        if user_integration_context := get_integration_context().get(self.user):
            if datetime.utcnow() > parser.parse(user_integration_context.get("refresh_token_expires_in"), ignoretz=True):
                # In case the refresh token expired we should generate a new one using the !cisco-webex-oauth-start command.
                raise DemistoException(
                    "The `refresh token` expired, please re-run the `!cisco-webex-oauth-start` command "
                    f"with the `user` argument set to {self.user}."
                )

            if datetime.utcnow() > parser.parse(user_integration_context.get("access_token_expires_in"), ignoretz=True):
                # In case the access token expired we create a new access token using the refresh token.
                result = self.create_access_token("refresh_token", refresh_token=user_integration_context.get("refresh_token"))
                self.save_tokens_to_integration_context(result)
                return result.get("access_token")  # Return the new access token from the API response.

            return user_integration_context.get("access_token")  # Return the access token from the integration context.

        return None

    def oauth_start(self) -> tuple[str, str]:
        """returns a URL as a string to use in the oauth start command."""
        params = assign_params(
            response_type="code",
            scope=self.scope,
            client_id=self.client_id,
            redirect_uri=self.redirect_uri,
        )
        return f'{urljoin(self._base_url, "authorize?")}{urllib.parse.urlencode(params, quote_via=urllib.parse.quote)}', self.user

    def oauth_complete(self, code: str | None):
        """
        Completes the authentication process.
        It gets a code returned from the `oauth_start` command and sets the access & refresh token.
        Args:
            code: The code return from the `oauth_start` command as a query parameter.
        """
        result = self.create_access_token("authorization_code", code=code)
        self.save_tokens_to_integration_context(result)

    @abstractmethod
    def oauth_test(self) -> str:
        """
        Abstract function to test the client connection with the API.
        """


class AdminClient(Client):
    def __init__(
        self,
        url: str,
        verify: bool,
        proxy: bool,
        client_id: str,
        client_secret: str,
        redirect_uri: str,
        scope: str | None,
        org_id: str,
    ):
        super().__init__(url, verify, proxy, client_id, client_secret, redirect_uri, scope, user="admin")
        self.org_id = org_id
        self._headers = {"Authorization": f"Bearer {self.get_access_token()}"}

    def oauth_test(self):
        """Runs the `get_admin_audits` method in order to tes the connection."""
        self.get_admin_audits(date_time_to_iso_format(datetime.utcnow() - timedelta(hours=3)))

    def get_admin_audits(self, from_date: str, limit: int = DEFAULT_MAX_FETCH, next_url: str = "") -> requests.Response:
        """
        Returns admin audit events either with a `next_url` or according to filter parameters.
        Args:
            from_date: A string represents a datetime is ISO format from when to get the events.
            limit: A number of how mny events to return.
            next_url: A URL (returned from the previous run) to get the events (using for pagination).

        Returns:
            A response object with the events returned dform the API.
        """
        if next_url:
            return self._http_request(method="GET", full_url=next_url, resp_type="response")
        params = {
            "orgId": self.org_id,
            "from": from_date,
            "to": date_time_to_iso_format(datetime.utcnow()),
            "max": min(limit, DEFAULT_MAX_FETCH),
        }
        return self._http_request(method="GET", url_suffix="adminAudit/events", params=params, resp_type="response")

    def get_security_audits(self, from_date: str, limit: int = DEFAULT_MAX_FETCH, next_url: str = "") -> requests.Response:
        """
        Returns admin security events either with a `next_url` or according to filter parameters.
        Args:
            from_date: A string represents a datetime is ISO format from when to get the events.
            limit: A number of how mny events to return.
            next_url: A URL (returned from the previous run) to get the events (using for pagination).

        Returns:
            A response object with the events returned dform the API.
        """
        if next_url:
            return self._http_request(method="GET", full_url=next_url, resp_type="response")
        params = {
            "orgId": self.org_id,
            "startTime": from_date,
            "endTime": date_time_to_iso_format(datetime.utcnow()),
            "max": min(limit, 1000),
        }
        return self._http_request(method="GET", url_suffix="admin/securityAudit/events", params=params, resp_type="response")


class ComplianceOfficerClient(Client):
    def __init__(
        self, url: str, verify: bool, proxy: bool, client_id: str, client_secret: str, redirect_uri: str, scope: str | None
    ):
        super().__init__(url, verify, proxy, client_id, client_secret, redirect_uri, scope, user="compliance_officer")
        self._headers = {"Authorization": f"Bearer {self.get_access_token()}"}

    def oauth_test(self):
        """Runs the `get_compliance_officer_events` method in order to tes the connection."""
        self.get_compliance_officer_events(date_time_to_iso_format(datetime.utcnow() - timedelta(hours=3)))

    def get_compliance_officer_events(
        self, from_date: str, limit: int = DEFAULT_MAX_FETCH, next_url: str = ""
    ) -> requests.Response:
        """
        Returns events either with a `next_url` or according to filter parameters.
        Args:
            from_date: A string represents a datetime is ISO format from when to get the events.
            limit: A number of how mny events to return.
            next_url: A URL (returned from the previous run) to get the events (using for pagination).

        Returns:
            A response object with the events returned dform the API.
        """
        if next_url:
            return self._http_request(method="GET", full_url=next_url, resp_type="response")
        params = {
            "from": from_date,
            "to": date_time_to_iso_format(datetime.utcnow()),
            "max": min(limit, 1000),
        }
        return self._http_request(method="GET", url_suffix="events", params=params, resp_type="response")


""" COMMAND FUNCTIONS """


def test_module():
    """Raises an exception with the message to run the `!cisco-webex-oauth-start` command."""
    raise DemistoException(
        "In order to authorize the instance, first run the command `!cisco-webex-oauth-start`, "
        "and complete the process in the URL that is returned. You will then be redirected "
        "to the callback URL. Copy the authorization code found in the query parameter "
        "`code`, and paste that value in the command `!cisco-webex-oauth-complete` as an argument to finish "
        "the process. Then you can test it bu running the `!cisco-webex-oauth-test` command."
    )


def oauth_start(client: Client) -> CommandResults:
    """
    Runs the client `oauth_start` method in order to create a URL to start the authenticate process.
    Args:
        client: A client object either an AdminClient or a ComplianceOfficerClient.

    Returns:
        A CommandResult with a URL generated according to the client attributes to start the authentication.
    """
    url, user = client.oauth_start()
    message = f"""
>### Authorization instructions
>1. Click on the [login URL]({url}) to sign in and grant Cortex XSOAR permissions for your Cisco Webex {user} application.
You will be automatically redirected to a link with the following structure:
```REDIRECT_URI?code=AUTH_CODE```
>2. Copy the `AUTH_CODE` (without the `code=` prefix)
and use it in **!cisco-webex-oauth-complete** command as a value fot the **code** argument.
"""
    return CommandResults(readable_output=message)


def oauth_complete(client: Client, args: dict) -> CommandResults:
    """
    Gets a code returned from the `oath_start` command as a query parameter and creates an access & refresh token to save them in
    the integration_context.
    Args:
        client: A client object either an AdminClient or a ComplianceOfficerClient.
        args: An authorization code provided as a query parameter called `code` returned from the `oauth_start` command.

    Returns:
        A CommandResult with a message that the tokens was created and saved successfully.
    """
    code = args.get("code")
    client.oauth_complete(code)
    return CommandResults(readable_output="Authorization completed successfully.")


def oauth_test(client: Client) -> CommandResults:
    """
    Runs the `oauth_test` command in order to test the connection.
    Args:
        client: A client object either an AdminClient or a ComplianceOfficerClient.

    Returns:
        A CommandResult with a message that the Test succeeded.
    """
    client.oauth_test()
    return CommandResults(readable_output="```✅ Success!```")


def get_events_with_pagination(client_function: Callable, from_date: str, limit: int, next_url: str = "") -> tuple[list, str]:
    """
    Returns events with pagination mechanism.
    Args:
        client_function: The function used to return the events (for each event type).
        from_date: A string represents a datetime is ISO format from when to get the events.
        limit: A number of how mny events to return.
        next_url: A URL (returned from the previous run) to get the events (using for pagination).

    Returns:
        A list of events and a string of the next_url (a URL to return events).
    """
    events: list[dict] = []

    response = client_function(from_date, limit, next_url)
    response_json = response.json()
    events.extend(response_json.get("items", []))

    while (next_url := demisto.get(response.links, "next.url", "")) and len(events) < limit:
        response = client_function(from_date, limit, next_url)
        response_json = response.json()
        events.extend(response_json.get("items", []))

    add_fields_to_events(events, evnet_type=COMMAND_FUNCTION_TO_EVENT_TYPE.get(client_function.__name__))

    return events, next_url


def get_events_command(command_function: Callable, args: dict) -> tuple[CommandResults, list]:
    """
    Returns a list of events
    Args:
        command_function: The function used to return the events (for each event type).
        args: A dict with fetch data.

    Returns:
        A CommandResult with a readable output of the events and a list of the events.
    """
    from_date = args.get("since_datetime", date_time_to_iso_format(datetime.utcnow() - timedelta(hours=3)))
    limit = arg_to_number(args.get("limit", 5)) or DEFAULT_MAX_FETCH

    events, _ = get_events_with_pagination(command_function, from_date=from_date, limit=limit)

    command_results = CommandResults(
        readable_output=tableToMarkdown(COMMAND_FUNCTION_TO_EVENT_TYPE.get(command_function.__name__), events)
    )
    return command_results, events


def fetch_events(
    admin_client: AdminClient,
    co_client: ComplianceOfficerClient,
    last_run: dict,
    max_fetch: int,
    fetch_security_audits: bool = False,
) -> tuple[list, dict]:
    """
    Fetches three types of events (Admin Audits, Security Audits, Events),
    It fetches from the latest event `create` date or with a `next_url` returned form the previous fetch,
    And saves the latest evnet create date and the `next_url` from the current fetch to use in the next fetch.
    Args:
        admin_client: An instance of the AdminClient.
        co_client: An instance of the ComplianceOfficesClient.
        last_run: A dict with the latest fetch data.
        max_fetch: A number of how many events to return per fetch.
        fetch_security_audits: A boolean that defines whether to return security_audits or not. (since it needs more permissions).

    Returns:
        A list of events and a dict with fetch info to use in the next fetch.
    """
    all_events = []

    if not last_run:
        last_run = create_last_run()

    demisto.debug(f"start fetching events with last_run: {last_run}")

    event_type_to_client_function = {
        "admin_audits": admin_client.get_admin_audits,
        "compliance_officer_events": co_client.get_compliance_officer_events,
    }
    if fetch_security_audits:
        event_type_to_client_function["security_audits"] = admin_client.get_security_audits

    for event_type, client_function in event_type_to_client_function.items():
        since_datetime = demisto.get(last_run, f"{event_type}.since_datetime")
        next_url = demisto.get(last_run, f"{event_type}.next_url", "")
        events, next_url = get_events_with_pagination(client_function, since_datetime, max_fetch, next_url)
        last_run[event_type]["next_url"] = next_url
        if events:
            last_run[event_type]["since_datetime"] = increase_datetime_for_next_fetch(events, since_datetime)
            all_events.extend(events)

    demisto.debug(f"finished fetching {len(all_events)} events, last_run will be set to: {last_run}")

    return all_events, last_run


""" MAIN FUNCTION """


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

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

    # parse parameters
    base_url = urljoin(params.get("base_url", "https://webexapis.com"), "/v1/")
    admin_client_id = demisto.get(params, "admin_credentials.identifier", "")
    admin_client_secret = demisto.get(params, "admin_credentials.password", "")
    admin_redirect_uri = params.get("admin_app_redirect_uri")
    admin_org_id = params.get("admin_org_id")
    compliance_officer_client_id = demisto.get(params, "compliance_officer_credentials.identifier", "")
    compliance_officer_client_secret = demisto.get(params, "compliance_officer_credentials.password", "")
    compliance_officer_redirect_uri = params.get("compliance_officer_redirect_uri")
    fetch_security_audits = argToBoolean(params.get("fetch_security_audit_events", False))
    verify_certificate = argToBoolean(not params.get("insecure", False))
    proxy = argToBoolean(params.get("proxy", False))
    max_fetch = arg_to_number(params.get("max_fetch", DEFAULT_MAX_FETCH)) or DEFAULT_MAX_FETCH
    if not 0 < max_fetch <= 2000:
        max_fetch = DEFAULT_MAX_FETCH
    user = args.get("user")

    demisto.debug(f"Command being called is {command}")

    try:
        admin_client = AdminClient(
            url=base_url,
            verify=verify_certificate,
            proxy=proxy,
            client_id=admin_client_id,
            client_secret=admin_client_secret,
            redirect_uri=admin_redirect_uri,
            org_id=admin_org_id,
            scope=SCOPE.get("admin"),
        )

        compliance_officer_client = ComplianceOfficerClient(
            url=base_url,
            verify=verify_certificate,
            proxy=proxy,
            client_id=compliance_officer_client_id,
            client_secret=compliance_officer_client_secret,
            redirect_uri=compliance_officer_redirect_uri,
            scope=SCOPE.get("compliance_officer"),
        )

        if command == "test-module":
            test_module()

        elif command == "cisco-webex-oauth-start":
            remove_integration_context_for_user(user)
            client = admin_client if user == "admin" else compliance_officer_client
            result = oauth_start(client)
            return_results(result)

        elif command == "cisco-webex-oauth-complete":
            client = admin_client if user == "admin" else compliance_officer_client
            result = oauth_complete(client, args)
            return_results(result)

        elif command == "cisco-webex-oauth-test":
            client = admin_client if user == "admin" else compliance_officer_client
            result = oauth_test(client)
            return_results(result)

        elif command == "cisco-webex-get-admin-audit-events":
            command_results, events = get_events_command(admin_client.get_admin_audits, args)
            if argToBoolean(args.get("should_push_events", False)):
                demisto.debug(f"Sending to XSIAM {len(events)} events of type admin_audits")
                send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            return_results(command_results)

        elif command == "cisco-webex-get-security-audit-events":
            command_results, events = get_events_command(admin_client.get_security_audits, args)
            if argToBoolean(args.get("should_push_events", False)):
                demisto.debug(f"Sending to XSIAM {len(events)} events of type security_audits")
                send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            return_results(command_results)

        elif command == "cisco-webex-get-compliance-officer-events":
            command_results, events = get_events_command(compliance_officer_client.get_compliance_officer_events, args)
            if argToBoolean(args.get("should_push_events", False)):
                demisto.debug(f"Sending to XSIAM {len(events)} events of type events")
                send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            return_results(command_results)

        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            events, next_run = fetch_events(admin_client, compliance_officer_client, last_run, max_fetch, fetch_security_audits)
            send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            demisto.setLastRun(next_run)

        else:
            raise NotImplementedError(f'Command "{command}" was not implemented.')

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


""" ENTRY POINT """

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