BeyondTrust Privilege Management Cloud

BeyondTrust Privilege Management Cloud (PM Cloud) integration for retrieving audit events and activity logs.

Endpoint · BeyondTrust

Details

IDBeyondTrust Privilege Management Cloud
ProviderBeyondTrust
CategoryEndpoint
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

BeyondTrust Privilege Management Cloud

Overview

BeyondTrust Privilege Management Cloud (PM Cloud) integration for retrieving audit events and activity logs from the BeyondTrust PM Cloud Management API v3.

This integration supports:

  • Fetching Activity Audits (audit logs of configuration changes and administrative actions)
  • Fetching Events (endpoint privilege management events)
  • Event collection for XSIAM

Configure BeyondTrust Privilege Management Cloud on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for BeyondTrust Privilege Management Cloud.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Server URL The server URL in the format: https://[yourProductionSub-domainName]-services.pm.beyondtrustcloud.com True
    Client ID The OAuth client ID from the API account created in PM Cloud. True
    Client Secret The OAuth client secret from the API account created in PM Cloud. True
    Events types to fetch Select which event types to fetch: Activity Audits, Events, or both. False
    Trust any certificate (not secure) When selected, certificates are not verified. False
    Use system proxy settings Use the system proxy settings. False
    Fetch events Enable event fetching for XSIAM. False
    Maximum number of audit events (per type) Maximum number of events to fetch per fetch cycle (default: 6000). False
    First fetch timestamp The time from which to start fetching events (e.g., “3 days”, “1 week”). False
  4. Click Test to validate the URLs, token, and connection.

Prerequisites

To create an API account in BeyondTrust PM Cloud:

  1. Sign into app.beyondtrust.io.
  2. From the top left of the page, click > Endpoint Privilege Management for Windows and Mac > Configuration.
  3. Click the Configuration menu, and then click API Settings.
  4. Click Create an API Account.
  5. Enter a name and description.
  6. The Client ID and Client Secret are automatically generated. The secret is only visible when initially generated for security reasons.
  7. Copy the Client ID and Client Secret values to use in the integration configuration.

Important Notes:

  • The instance URL can be found at the top of the API Settings page.
  • The client secret cannot be modified, but it can be regenerated on the Configuration > Settings > API Settings page.
  • Regenerating a client secret immediately invalidates any OAuth tokens associated with the account.

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.

beyondtrust-pm-cloud-get-events


Retrieves events or activity audits from BeyondTrust PM Cloud. Use the event_type argument to switch between Events and Activity Audits.

Base Command

beyondtrust-pm-cloud-get-events

Input

Argument Name Description Required
event_type The type of events to retrieve. Possible values are: Events, Activity Audits. Default is Events. Optional
start_date Start Date (UTC) to search events from (Elastic Ingestion Timestamp in UTC). Format: 2022-08-12T17:34:28.694Z. If not provided, defaults to 1 hour ago. Optional
limit Maximum records that can be returned. For Events: max 1000. For Activity Audits: max 200. Default is 50. Optional
should_push_events Set to true to push events to XSIAM. Used for debugging. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
BeyondTrust.Event.id String The ID of the event.
BeyondTrust.Event.created Date The creation time of the event.
BeyondTrust.Event.@timestamp Date The timestamp of the event.
BeyondTrust.Event.auditType String Audit type name (Activity Audits only).
BeyondTrust.Event.details String Details of the activity (Activity Audits only).
BeyondTrust.Event.entity String Name of Activity Audit entity (Activity Audits only).
BeyondTrust.Event.user String Initiated user email or API client identifier (Activity Audits only).
BeyondTrust.Event.changedBy String Audit ChangedBy - API or Portal (Activity Audits only).

Command example (Events)

!beyondtrust-pm-cloud-get-events event_type="Events" start_date="2025-01-01T00:00:00.000Z" limit="10"

Context Example (Events)

{
    "BeyondTrust": {
        "Event": [
            {
                "id": "event-123",
                "created": "2025-01-01T00:00:00.000Z",
                "@timestamp": "2025-01-01T00:00:00.000Z"
            }
        ]
    }
}

Command example (Activity Audits)

!beyondtrust-pm-cloud-get-events event_type="Activity Audits" limit="10"

Context Example (Activity Audits)

{
    "BeyondTrust": {
        "Event": [
            {
                "id": 123,
                "created": "2025-01-01T00:00:00.000Z",
                "details": "User created",
                "user": "admin@example.com",
                "entity": "User",
                "auditType": "Create",
                "changedBy": "Portal"
            }
        ]
    }
}

Event Collection

This integration supports event collection for XSIAM. When configured with Fetch events enabled, the integration will:

  1. Fetch Activity Audits and/or Events based on the Events types to fetch configuration.
  2. Add required XSIAM fields to each event:
    • _time: Set from the event’s created or @timestamp field
    • source_log_type: Set to activity_audits or events depending on the event type
    • vendor: Set to beyondtrust
    • product: Set to pm_cloud

Continuous Event Coverage

Important: The integration uses a continuous coverage approach to prevent event gaps:

  • Each fetch cycle stores the fetch start time (not the last event’s timestamp) as the next run’s start time
  • This ensures events created between fetch cycles are never missed
  • Example: If Fetch 1 runs at 12:00 and finds events up to 11:50, it stores 12:00 as the next start time
  • When Fetch 2 runs at 12:05, it fetches from 12:00 to 12:05, capturing any events created in that window
  • Deduplication is performed based on event IDs to handle any overlapping events

Event Types

Activity Audits

Activity Audits track configuration changes and administrative actions in PM Cloud, including:

  • User management (create, modify, disable)
  • Group management
  • Policy changes
  • Computer management
  • API client changes
  • Installation key changes
  • Settings modifications

Dataset: beyondtrust_pm_cloud_raw
Source Log Type: activity_audits

Events

Events track endpoint privilege management activities, including:

  • Application execution events
  • Privilege elevation events
  • Authorization requests
  • Policy enforcement events
  • Security events

Dataset: beyondtrust_pm_cloud_raw
Source Log Type: events

API Documentation

This integration uses the BeyondTrust PM Cloud Management API v3. For more information, refer to:

Known Limitations

  • The Events endpoint (FromStartDate) has a maximum record size of 1000 per request.
  • The Activity Audits endpoint has a maximum page size of 200 records.
  • This integration is developed for blind deployment and will be tested in the customer’s environment.

Troubleshooting

Authentication Issues

  • Verify the Client ID and Client Secret are correct.
  • Ensure the API account has the necessary permissions in PM Cloud.
  • Check that the Server URL is in the correct format: https://[yourProductionSub-domainName]-services.pm.beyondtrustcloud.com

No Events Fetched

  • Verify the First fetch timestamp is set appropriately.
  • Check that events exist in the specified time range.
  • Ensure Events types to fetch is configured correctly.
  • Review the integration logs for any API errors.

Token Expiration

  • OAuth tokens expire after 3600 seconds (1 hour).
  • The integration automatically refreshes tokens as needed.
  • If you regenerate the client secret in PM Cloud, update the integration configuration immediately.

Configuration parameters

  • url — Server URL (required)
  • credentials — Client ID (required)
  • events_types_to_fetch — Events types to fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch events
  • max_fetch — Maximum number of events (per type)
  • first_fetch — First fetch timestamp

Commands (1)

  • beyondtrust-pm-cloud-get-events

    This command is used for developing/debugging and is to be used with caution, as it can create events, leading to events duplication and API request limitation exceeding. Retrieves events or activity audits from BeyondTrust PM Cloud.

import traceback

import demistomock as demisto
from CommonServerPython import *
from typing import Any

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
VENDOR = "beyondtrust"
PRODUCT = "pm_cloud"
DEFAULT_LIMIT = 1000
DEFAULT_PAGE_SIZE = 200
INTEGRATION_NAME = "BeyondTrust PM Cloud"

""" CLIENT CLASS """


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

    Args:
        base_url (str): The base URL of the service.
        client_id (str): The client ID for authentication.
        client_secret (str): The client secret for authentication.
        verify (bool): Whether to verify the SSL certificate.
        proxy (bool): Whether to use a proxy.
    """

    def __init__(self, base_url: str, client_id: str, client_secret: str, verify: bool, proxy: bool):
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        self.client_id = client_id
        self.client_secret = client_secret
        self.token = None

    def get_token(self):
        """Retrieves the access token from the service."""
        demisto.debug(f"{INTEGRATION_NAME}: Attempting to get OAuth token from {self._base_url}")

        # Pass data as a dict so that requests handles URL-encoding correctly,
        # preserving characters like '=' in base64-encoded secrets.
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
        }

        headers = {"Content-Type": "application/x-www-form-urlencoded"}

        result = self._http_request(
            method="POST",
            url_suffix="/oauth/connect/token",
            data=data,
            headers=headers,
            timeout=30,
        )

        demisto.debug(f"{INTEGRATION_NAME}: Token request successful")
        return result.get("access_token")

    def _http_request(  # type: ignore[override]
        self,
        method: str,
        url_suffix: str = "",
        params: dict | None = None,
        data: dict | str | None = None,
        headers: dict | None = None,
        resp_type: str = "json",
        timeout: int = 10,
        **kwargs,
    ) -> Any:
        """Wrapper for http_request to handle authentication and execution."""
        demisto.debug(f"{INTEGRATION_NAME}: Sending request: method={method}, url_suffix={url_suffix}, params={params}")
        if not headers:
            headers = {}

        if url_suffix != "/oauth/connect/token":
            if not self.token:
                self.token = self.get_token()
            headers["Authorization"] = f"Bearer {self.token}"

        return super()._http_request(
            method=method,
            url_suffix=url_suffix,
            params=params,
            data=data,
            headers=headers,
            resp_type=resp_type,
            timeout=timeout,
            **kwargs,
        )

    def get_events(self, start_date: str, limit: int = DEFAULT_LIMIT) -> dict:
        """Retrieves events from the service.

        Args:
            start_date (str): The start date to retrieve events from.
            limit (int): The maximum number of events to retrieve. Max is 1000.

        Returns:
            dict: The response from the service.
        """
        # The BeyondTrust Events API enforces RecordSize between 1 and 1000
        params = {"StartDate": start_date, "RecordSize": min(limit, DEFAULT_LIMIT)}
        demisto.debug(f"{INTEGRATION_NAME}: Getting events with params: {params}")
        return self._http_request(method="GET", url_suffix="/management-api/v3/Events/FromStartDate", params=params)

    def get_audit_activity(
        self,
        page_size: int = DEFAULT_PAGE_SIZE,
        page_number: int = 1,
        filter_created_dates: list[str] | None = None,
        filter_created_selection_mode: str | None = None,
    ) -> dict:
        """Retrieves audit activity from the service.

        Args:
            page_size (int): The number of records per page.
            page_number (int): The page number to retrieve.
            filter_created_dates (list[str]): The dates to filter by.
            filter_created_selection_mode (str): The selection mode for the dates.

        Returns:
            dict: The response from the service.
        """
        params: dict[str, Any] = {"Pagination.PageSize": page_size, "Pagination.PageNumber": page_number}
        if filter_created_dates:
            params["Filter.Created.Dates"] = filter_created_dates
        if filter_created_selection_mode:
            params["Filter.Created.SelectionMode"] = filter_created_selection_mode

        demisto.debug(f"{INTEGRATION_NAME}: Getting audit activity with params: {params}")
        return self._http_request(method="GET", url_suffix="/management-api/v3/ActivityAudits/Details", params=params)


""" HELPER FUNCTIONS """


def get_dedup_key(event: dict) -> str:
    """Generates a deduplication key for an event.

    Events and Activity Audits may have overlapping IDs, so the event type
    (source_log_type) is included in the key to prevent cross-type collisions.

    Args:
        event (dict): The event to generate the key for.

    Returns:
        str: The deduplication key.
    """
    event_type = event.get("source_log_type", "N/A")
    if "id" in event:
        return f"{event_type}_{event['id']}"
    # Fallback to hashing the event content if no ID is present (unlikely based on docs but good practice)
    return f"{event_type}_{hash(json.dumps(event, sort_keys=True))}"


""" COMMAND FUNCTIONS """


def test_module(client: Client) -> str:
    """Tests API connectivity.

    Args:
        client (Client): The client to use.

    Returns:
        str: 'ok' if the test passed, anything else otherwise.
    """
    try:
        client.get_token()
        return "ok"
    except Exception as e:
        return f"Failed to connect to the service: {e}"


def get_events_command(client: Client, args: dict) -> CommandResults:
    """Retrieves events or activity audits from the service based on event_type.

    Args:
        client (Client): The client to use.
        args (dict): The command arguments.

    Returns:
        CommandResults: The command results.
    """
    event_type = args.get("event_type", "Events")
    start_date = args.get("start_date")
    limit = arg_to_number(args.get("limit", 50)) or 50
    should_push_events = argToBoolean(args.get("should_push_events", False))

    if event_type == "Activity Audits":
        # For Activity Audits, use the audit activity API
        limit = min(limit, DEFAULT_PAGE_SIZE)

        if not start_date:
            start_date = (datetime.now(timezone.utc) - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S.%f")
        else:
            # Convert ISO format to the format expected by the audit API
            parsed = dateparser.parse(start_date)
            if parsed:
                start_date = parsed.strftime("%Y-%m-%d %H:%M:%S.%f")

        end_date = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")

        response = client.get_audit_activity(
            page_size=limit,
            page_number=1,
            filter_created_dates=[start_date, end_date],
            filter_created_selection_mode="Range",
        )
        events = response.get("data", [])
        source_log_type = "activity_audits"
    else:
        # For Events, use the events API
        limit = min(limit, DEFAULT_LIMIT)

        if not start_date:
            start_date = (datetime.now(timezone.utc) - timedelta(hours=1)).strftime(DATE_FORMAT)

        response = client.get_events(start_date, limit)
        events = response.get("events", [])
        source_log_type = "events"

    # Add XSIAM fields if pushing events
    if should_push_events:
        for event in events:
            event["_time"] = event.get("created") or event.get("@timestamp")
            event["source_log_type"] = source_log_type
            event["vendor"] = VENDOR
            event["product"] = PRODUCT
        send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)

    return CommandResults(outputs_prefix="BeyondTrust.Event", outputs_key_field="id", outputs=events, raw_response=response)


def fetch_pm_events(
    client: Client, last_run: dict, first_fetch: str, max_fetch: int, fetch_end_time: datetime
) -> tuple[dict, list[dict]]:
    """Fetches PM Cloud events from the Events API.

    The Events API has a max RecordSize of 1000 per request. If max_fetch > 1000,
    this function makes multiple API calls, using the last event's timestamp as the
    next StartDate to paginate through all available events.

    Args:
        client (Client): The client to use.
        last_run (dict): The last run context.
        first_fetch (str): The first fetch time.
        max_fetch (int): The maximum number of events to fetch for this event type.
        fetch_end_time (datetime): The end time for this fetch cycle.

    Returns:
        tuple[dict, list[dict]]: The updated run context and the fetched events.
    """
    demisto.debug(
        f"{INTEGRATION_NAME}: Starting fetch_pm_events. " f"last_run={last_run}, first_fetch={first_fetch}, max_fetch={max_fetch}"
    )
    next_run = last_run.copy()
    last_event_time_str = last_run.get("last_event_time")

    if not last_event_time_str:
        last_event_time = dateparser.parse(first_fetch).replace(tzinfo=timezone.utc)  # type: ignore
    else:
        last_event_time = dateparser.parse(last_event_time_str).replace(tzinfo=timezone.utc)  # type: ignore

    # Format the start date as required by the API: ISO 8601
    start_date_str = last_event_time.strftime(DATE_FORMAT)

    all_fetched_events: list[dict] = []
    total_fetched = 0

    while total_fetched < max_fetch:
        # The Events API enforces RecordSize between 1 and 1000
        remaining = max_fetch - total_fetched
        batch_size = min(remaining, DEFAULT_LIMIT)

        demisto.debug(f"{INTEGRATION_NAME}: Fetching events batch from {start_date_str}, batch_size={batch_size}")
        response = client.get_events(start_date_str, batch_size)
        fetched_events = response.get("events", [])
        demisto.debug(f"{INTEGRATION_NAME}: Fetched {len(fetched_events)} PM events in this batch.")

        if not fetched_events:
            break

        for event in fetched_events:
            # Add fields for XSIAM
            event["_time"] = event.get("created") or event.get("@timestamp")
            event["source_log_type"] = "events"
            event["vendor"] = VENDOR
            event["product"] = PRODUCT

        all_fetched_events.extend(fetched_events)
        total_fetched += len(fetched_events)

        # If we got fewer events than requested, there are no more events to fetch
        if len(fetched_events) < batch_size:
            break

        # Use the last event's timestamp as the next StartDate to continue pagination.
        # Parse and reformat to ensure the date format matches what the API expects.
        last_event_timestamp = fetched_events[-1].get("created") or fetched_events[-1].get("@timestamp")
        if not last_event_timestamp:
            demisto.debug(f"{INTEGRATION_NAME}: No timestamp found on last event, stopping pagination.")
            break
        parsed_ts = dateparser.parse(last_event_timestamp)
        if not parsed_ts:
            demisto.debug(f"{INTEGRATION_NAME}: Could not parse timestamp '{last_event_timestamp}', stopping pagination.")
            break
        start_date_str = parsed_ts.strftime(DATE_FORMAT)
        demisto.debug(f"{INTEGRATION_NAME}: Continuing pagination from {start_date_str}")

    demisto.debug(f"{INTEGRATION_NAME}: Total PM events fetched: {total_fetched}")
    if all_fetched_events:
        demisto.debug(f"{INTEGRATION_NAME}: Sample PM event (first): {all_fetched_events[0]}")
        if len(all_fetched_events) > 1:
            demisto.debug(f"{INTEGRATION_NAME}: Sample PM event (last): {all_fetched_events[-1]}")

    # Store the fetch end time as the next start time to ensure continuous coverage
    next_run["last_event_time"] = fetch_end_time.strftime(DATE_FORMAT)

    return next_run, all_fetched_events


def fetch_activity_audits(
    client: Client, last_run: dict, first_fetch: str, max_fetch: int, fetch_end_time: datetime
) -> tuple[dict, list[dict]]:
    """Fetches Activity Audit events from the Activity Audits API.

    Args:
        client (Client): The client to use.
        last_run (dict): The last run context.
        first_fetch (str): The first fetch time.
        max_fetch (int): The maximum number of events to fetch for this event type.
        fetch_end_time (datetime): The end time for this fetch cycle.

    Returns:
        tuple[dict, list[dict]]: The updated run context and the fetched audit events.
    """
    demisto.debug(
        f"{INTEGRATION_NAME}: Starting fetch_activity_audits. "
        f"last_run={last_run}, first_fetch={first_fetch}, max_fetch={max_fetch}"
    )
    next_run = last_run.copy()
    last_audit_time_str = last_run.get("last_audit_time")

    if not last_audit_time_str:
        last_audit_time = dateparser.parse(first_fetch).replace(tzinfo=timezone.utc)  # type: ignore
    else:
        last_audit_time = dateparser.parse(last_audit_time_str).replace(tzinfo=timezone.utc)  # type: ignore

    # Activity Audits API uses pagination and filtering.
    # We fetch from last_audit_time to fetch_end_time to ensure continuous coverage.
    # The API documentation says "Filter.Created.Dates: array of date-times".
    # And "Filter.Created.SelectionMode: Range" - this requires two dates for a range.

    start_date_str = last_audit_time.strftime("%Y-%m-%d %H:%M:%S.%f")
    end_date_str = fetch_end_time.strftime("%Y-%m-%d %H:%M:%S.%f")

    page_number = 1
    total_fetched_audits = 0
    fetched_audits_list: list[dict] = []

    while True:
        # Calculate remaining limit for this cycle
        remaining_limit = max_fetch - total_fetched_audits
        if remaining_limit <= 0:
            break

        current_page_size = min(DEFAULT_PAGE_SIZE, remaining_limit)

        demisto.debug(f"{INTEGRATION_NAME}: Fetching page {page_number} of audits. Page size: {current_page_size}")
        response = client.get_audit_activity(
            page_size=current_page_size,
            page_number=page_number,
            filter_created_dates=[start_date_str, end_date_str],
            filter_created_selection_mode="Range",
        )

        fetched_audits = response.get("data", [])
        if not fetched_audits:
            break

        for audit in fetched_audits:
            # Add fields for XSIAM
            audit["_time"] = audit.get("created")
            audit["source_log_type"] = "activity_audits"
            audit["vendor"] = VENDOR
            audit["product"] = PRODUCT

        fetched_audits_list.extend(fetched_audits)
        total_fetched_audits += len(fetched_audits)
        demisto.debug(
            f"{INTEGRATION_NAME}: Fetched {len(fetched_audits)} audits in this page. " f"Total so far: {total_fetched_audits}"
        )
        if fetched_audits:
            demisto.debug(f"{INTEGRATION_NAME}: Sample audit from page {page_number} (first): {fetched_audits[0]}")

        # Check if we have more pages
        # The response has "pageCount" and "totalRecordCount"
        if page_number >= response.get("pageCount", 0):
            break

        page_number += 1

    demisto.debug(f"{INTEGRATION_NAME}: Finished fetching activity audits. Total fetched: {total_fetched_audits}")
    if fetched_audits_list:
        demisto.debug(f"{INTEGRATION_NAME}: Sample audit (first overall): {fetched_audits_list[0]}")
        if len(fetched_audits_list) > 1:
            demisto.debug(f"{INTEGRATION_NAME}: Sample audit (last overall): {fetched_audits_list[-1]}")

    # Store the fetch end time as the next start time to ensure continuous coverage
    next_run["last_audit_time"] = fetch_end_time.strftime(DATE_FORMAT)

    return next_run, fetched_audits_list


def fetch_events(
    client: Client, last_run: dict, first_fetch: str, max_fetch: int, events_types_to_fetch: list[str]
) -> tuple[dict, list[dict]]:
    """Fetches events from the service.

    This is the main orchestrator function that calls the appropriate fetch functions
    based on the event types configured to fetch.

    Args:
        client (Client): The client to use.
        last_run (dict): The last run context.
        first_fetch (str): The first fetch time.
        max_fetch (int): The maximum number of events to fetch per event type.
        events_types_to_fetch (list[str]): The types of events to fetch.

    Returns:
        tuple[dict, list[dict]]: The next run context and the fetched events.
    """
    demisto.debug(f"{INTEGRATION_NAME}: Starting fetch_events. events_types_to_fetch={events_types_to_fetch}")
    events: list[dict] = []
    next_run = last_run.copy()

    # Capture the current time at the start of this fetch cycle
    # This will be used as the start time for the next fetch to ensure no gaps
    fetch_end_time = datetime.now(timezone.utc)

    # Handle Events - max_fetch applies per event type
    if "Events" in events_types_to_fetch:
        next_run, fetched_events = fetch_pm_events(client, next_run, first_fetch, max_fetch, fetch_end_time)
        events.extend(fetched_events)

    # Handle Activity Audits - max_fetch applies per event type
    if "Activity Audits" in events_types_to_fetch:
        next_run, fetched_audits = fetch_activity_audits(client, next_run, first_fetch, max_fetch, fetch_end_time)
        events.extend(fetched_audits)

    return next_run, events


""" MAIN FUNCTION """


def main():
    """Main function to handle the command execution."""
    params = demisto.params()
    args = demisto.args()
    command = demisto.command()

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

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

    try:
        if command == "test-module":
            return_results(test_module(client))

        elif command == "beyondtrust-pm-cloud-get-events":
            return_results(get_events_command(client, args))

        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            first_fetch = params.get("first_fetch", "1 minute")
            max_fetch = arg_to_number(params.get("max_fetch", 6000)) or 6000
            events_types_to_fetch = argToList(params.get("events_types_to_fetch", "Activity Audits,Events"))

            next_run, events = fetch_events(client, last_run, first_fetch, max_fetch, events_types_to_fetch)

            # Deduplicate events
            deduped_events = {get_dedup_key(event): event for event in events}
            final_events = list(deduped_events.values())

            demisto.debug(f"{INTEGRATION_NAME}: Total events before dedup: {len(events)}, after dedup: {len(final_events)}")
            if final_events:
                demisto.debug(f"{INTEGRATION_NAME}: Sample final event (first): {final_events[0]}")
                if len(final_events) > 1:
                    demisto.debug(f"{INTEGRATION_NAME}: Sample final event (last): {final_events[-1]}")
            demisto.debug(f"{INTEGRATION_NAME}: Next run state: {next_run}")

            # Send events to XSIAM
            send_events_to_xsiam(vendor=VENDOR, product=PRODUCT, events=final_events)
            demisto.debug(f"{INTEGRATION_NAME}: Successfully sent {len(final_events)} events to XSIAM.")
            demisto.setLastRun(next_run)

    except Exception as error:
        error_msg = f"Failed to execute {command}. Error: {str(error)}"
        demisto.error(f"{error_msg}\n{traceback.format_exc()}")
        return_error(error_msg)


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