ExabeamSecOpsPlatform

Exabeam Security Operations Platform offers a centralized and scalable platform for log management.

Analytics & SIEM · Exabeam Security Operations Platform

Details

IDExabeamSecOpsPlatform
ProviderExabeam
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Exabeam Security Operations Platform offers a centralized and scalable platform for log management.
This integration was integrated and tested with version v1.0 of ExabeamSecOpsPlatform.

Configure Exabeam Security Operations Platform in Cortex

Parameter Description Required
Server URL   True
Client ID   True
Client Secret   True
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch incidents Supported on Cortex XSOAR only. False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) Supported on Cortex XSOAR only. False
Maximum Incidents Per Fetch Supported on Cortex XSOAR only. This value should not exceed 3,000 due to product’s API limitations. False
Fetch query Supported on Cortex XSOAR only. In the key:value. For example: NOT stage:"CLOSED". False
Incident type Supported on Cortex XSOAR only. False
Fetch events Supported on Cortex XSIAM only. False
Maximum Number of Cases Per Fetch Supported on Cortex XSIAM only. 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.

exabeam-platform-event-search


Get events from Exabeam Security Operations Platform.

Base Command

exabeam-platform-event-search

Input

Argument Name Description Required
start_time The starting date for the search range. Required
end_time The ending date for the search range. Required
query Query, using Lucene syntax, filters log data for precise analysis, without escaping and with values unquoted. e.g., query=”product: Correlation Rule AND rule_severity: High”. Optional
fields Comma-separated list of fields to be returned from the search. Optional
group_by Comma-separated list of fields by which to group the results. Optional
limit The maximal number of results to return. Maximum value is 3000. Optional

Context Output

Path Type Description
ExabeamPlatform.Event.id String The unique identifier associated with the event.
ExabeamPlatform.Event.rawLogIds String The raw log identifiers associated with the event.
ExabeamPlatform.Event.tier String The tier associated with the event.
ExabeamPlatform.Event.parsed String Whether the event has been parsed.
ExabeamPlatform.Event.rawLogs String The raw logs associated with the event.

Command example

!exabeam-platform-event-search end_time="today" start_time="7 days ago" limit=2 query="product: Correlation Rule AND rule_severity: High"

Context Example

{
    "ExabeamPlatform": {
        "Event": [
            {
                "approxLogTime": 1715694190909000,
                "collector_timestamp": 1715694190909000,
                "customFieldsJSON": "{}",
                "id": "fake",
                "ingest_time": 1715694222815000,
                "metadataFieldsJSON": "{\"m_collector_id\":\"aae1627e-8637-4597-9f43-e49a703a6151\",\"m_collector_name\":\"exa-cribl-logs-sm_exa_ws\",\"m_collector_type\":\"cribl-logs\"}",
                "parsed": false,
                "rawLogIds": [
                    "log-fic"
                ],
                "rawLogs": [
                    "ANY rawLog"
                ],
                "raw_log_size": 9,
                "tier": "Tier 4"
            },
            {
                "approxLogTime": 1715694915916000,
                "collector_timestamp": 1715694915916000,
                "customFieldsJSON": "{}",
                "id": "fictive-id",
                "ingest_time": 1715694946775000,
                "metadataFieldsJSON": "{\"m_collector_id\":\"aae1627e-8637-4597-9f43-e49a703a6151\",\"m_collector_name\":\"exa-cribl-logs-sm_exa_ws\",\"m_collector_type\":\"cribl-logs\"}",
                "parsed": false,
                "rawLogIds": [
                    "rawLogId"
                ],
                "rawLogs": [
                    "CONNECT hotmail"
                ],
                "raw_log_size": 59,
                "tier": "Tier 4"
            }
        ]
    }
}

Human Readable Output

Logs

Id Is Parsed Raw Log Ids Raw Logs Tier
fake false log-fic ANY rawLog Tier 4
fictive-id false rawLogId CONNECT hotmail Tier 4

exabeam-platform-table-record-list


Retrieve the records for a specific context table.

Base Command

exabeam-platform-table-record-list

Input

Argument Name Description Required
table_id ID of the table. Obtain this value by running exabeam-platform-context-table-list. Required
limit The number of records to return. Default is 50. Optional

Context Output

There is no context output for this command.

exabeam-platform-table-record-create


Add one or more context records directly to an existing table.

Base Command

exabeam-platform-table-record-create

Input

Argument Name Description Required
table_id ID of the table. Obtain this value by running exabeam-platform-context-table-list. Required
attributes A key-value map of record attributes. Required
operation Options for how data should be uploaded to an existing table. Possible values are: append, replace. Default is append. Optional
interval_in_seconds The interval in seconds between each poll. Default is 30. Optional
timeout The timeout in seconds until polling ends. Default is 600. Optional
tracker_id Specify the tracker ID from an upload request whose progress you want to track. Optional
hide_polling_output Suppresses the output of polling operations to reduce clutter in logs. Optional

Context Output

There is no context output for this command.

exabeam-platform-alert-search


Search for alerts that match one or more search criteria.

Base Command

exabeam-platform-alert-search

Input

Argument Name Description Required
alert_id Unique ID that identifies an alert. Optional
start_time Timestamp to start the search. Default is 7 days ago. Optional
end_time Timestamp to end the search. Default is today. Optional
query Query, using Lucene syntax, filters log data for precise analysis. Optional
fields List of fields to be returned from the search. Optional
order_by Order results by a specified field in ASC or DESC order, such as “riskScore ASC” or “riskScore DESC”. Optional
limit Limit the number of results returned from the search request. Default is 50. Optional
all_results If set to ‘True’, retrieves all available results, ignoring the limit parameter. Possible values are: True, False. Default is False. Optional
include_related_rules If set to ‘True’, filters the context to include the “rules” array related to the cases in the results. Possible values are: True, False. Default is False. Optional

Context Output

Path Type Description
ExabeamPlatform.Alert.alertDescriptionRt String The description of the alert in real-time.
ExabeamPlatform.Alert.alertId String The unique identifier of the alert.
ExabeamPlatform.Alert.alertName String The name or title of the alert.
ExabeamPlatform.Alert.approxLogTime Date The approximate log time of the alert.
ExabeamPlatform.Alert.assignee String The person assigned to the alert.
ExabeamPlatform.Alert.caseCreationTimestamp Number The timestamp when the case was created.
ExabeamPlatform.Alert.caseId String The unique identifier of the case associated with the alert.
ExabeamPlatform.Alert.creationBy String The user who created the alert.
ExabeamPlatform.Alert.creationTimestamp Date The timestamp when the alert was created.
ExabeamPlatform.Alert.destEndpoints Unknown The destination endpoints involved in the alert.
ExabeamPlatform.Alert.destHosts Unknown The destination hosts involved in the alert.
ExabeamPlatform.Alert.destIps Unknown The destination IP addresses involved in the alert.
ExabeamPlatform.Alert.groupedbyKey String The key used for grouping the alert.
ExabeamPlatform.Alert.groupedbyValue String The value used for grouping the alert.
ExabeamPlatform.Alert.groupingRuleId String The ID of the rule used for grouping the alert.
ExabeamPlatform.Alert.hasAttachments Boolean Indicates if the alert has attachments.
ExabeamPlatform.Alert.ingestTimestamp Date The timestamp when the alert was ingested into the system.
ExabeamPlatform.Alert.lastModifiedBy String The user who last modified the alert.
ExabeamPlatform.Alert.lastModifiedTimestamp Date The timestamp when the alert was last modified.
ExabeamPlatform.Alert.mitres.tactic String The MITRE tactic associated with the alert.
ExabeamPlatform.Alert.mitres.tacticKey String The MITRE tactic key associated with the alert.
ExabeamPlatform.Alert.mitres.technique String The MITRE technique associated with the alert.
ExabeamPlatform.Alert.mitres.techniqueKey String The MITRE technique key associated with the alert.
ExabeamPlatform.Alert.priority String The priority level of the alert.
ExabeamPlatform.Alert.products String The products involved in the alert.
ExabeamPlatform.Alert.queue String The queue in which the alert is placed.
ExabeamPlatform.Alert.riskScore Number The risk score associated with the alert.
ExabeamPlatform.Alert.srcEndpoints.ip String The IP addresses of the source endpoints involved in the alert.
ExabeamPlatform.Alert.srcHosts Unknown The source hosts involved in the alert.
ExabeamPlatform.Alert.srcIps String The source IP addresses involved in the alert.
ExabeamPlatform.Alert.stage String The stage of the alert in the investigation process.
ExabeamPlatform.Alert.status String The status of the alert.
ExabeamPlatform.Alert.subscriptionCode String The subscription code associated with the alert.
ExabeamPlatform.Alert.tags Unknown The tags associated with the alert.
ExabeamPlatform.Alert.useCases String The use cases related to the alert.
ExabeamPlatform.Alert.users Unknown The users involved in the alert.
ExabeamPlatform.Alert.vendors String The vendors associated with the alert.

exabeam-platform-context-table-delete


Delete a specific context table, including records and attributes.

Base Command

exabeam-platform-context-table-delete

Input

Argument Name Description Required
table_id Specify the ID of an existing context table. Required
delete_unused_custom_attributes Delete any custom attributes in this table that are not used in another context table. Possible values are: True, False. Default is False. Optional

Context Output

There is no context output for this command.

exabeam-platform-context-table-list


Retrieve metadata for all existing context tables, including source, operational status, and attribute mapping.

Base Command

exabeam-platform-table-record-list

Input

Argument Name Description Required
table_id Specify the ID of an existing context table. Optional
limit Limit the number of results returned from the request. Default is 50. Optional
include_attributes If set to ‘True’, filters the context to include the “attributes” array related to the cases in the results. Possible values are: True, False. Default is False. Optional

Context Output

Path Type Description
ExabeamPlatform.ContextTable.attributeMapping Unknown The attribute mapping of the context table.
ExabeamPlatform.ContextTable.attributes.displayName String The display name of the attribute.
ExabeamPlatform.ContextTable.attributes.id String The unique identifier of the attribute.
ExabeamPlatform.ContextTable.attributes.isKey Boolean Indicates if the attribute is a key attribute.
ExabeamPlatform.ContextTable.attributes.type String The type of the attribute.
ExabeamPlatform.ContextTable.contextType String The type of context the table represents.
ExabeamPlatform.ContextTable.id String The unique identifier of the context table.
ExabeamPlatform.ContextTable.lastUpdated Number The timestamp of the last update to the context table.
ExabeamPlatform.ContextTable.name String The name of the context table.
ExabeamPlatform.ContextTable.source String The source of the context table data.
ExabeamPlatform.ContextTable.status String The status of the context table.
ExabeamPlatform.ContextTable.totalItems Number The total number of items in the context table.

exabeam-platform-case-search


Search for cases that match one or more search criteria. For example, you can search for cases that are associated with a specific caseId and that reference specific rules.

Base Command

exabeam-platform-case-search

Input

Argument Name Description Required
case_id An optional case id parameter to get a specific case. Optional
start_time Timestamp to start the search. Default is 7 days ago. Optional
end_time Timestamp to end the search. Default is today. Optional
query Query, using Lucene syntax, filters log data for precise analysis. Optional
fields List of fields to be returned from the search. Optional
order_by Order results by a specified field in ASC or DESC order, such as “riskScore ASC” or “riskScore DESC”. Optional
limit Limit the number of results returned from the search request. Default is 50. Optional
all_results If set to ‘True’, retrieves all available results, ignoring the limit parameter. Possible values are: True, False. Default is False. Optional
include_related_rules If set to ‘True’, filters the context to include the “rules” array related to the cases in the results. Possible values are: True, False. Default is False. Optional

Context Output

Path Type Description
ExabeamPlatform.Case.alertId String Unique identifier for the alert associated with the case.
ExabeamPlatform.Case.alertName String Name of the alert associated with the case.
ExabeamPlatform.Case.approxLogTime Number Approximate log time of the event that generated the case.
ExabeamPlatform.Case.assignee String User assigned to the case.
ExabeamPlatform.Case.caseCreationTimestamp Number Timestamp when the case was created.
ExabeamPlatform.Case.caseId String Unique identifier for the case.
ExabeamPlatform.Case.destHosts Unknown Destination hosts involved in the case.
ExabeamPlatform.Case.destIps Unknown Destination IP addresses involved in the case.
ExabeamPlatform.Case.groupedbyKey String Key by which the case was grouped.
ExabeamPlatform.Case.groupedbyValue String Value by which the case was grouped.
ExabeamPlatform.Case.hasAttachments Boolean Indicates if the case has attachments.
ExabeamPlatform.Case.ingestTimestamp Unknown Timestamp when the case was ingested.
ExabeamPlatform.Case.lastModifiedTimestamp Unknown Timestamp when the case was last modified.
ExabeamPlatform.Case.mitres Unknown MITRE tactics and techniques associated with the case.
ExabeamPlatform.Case.priority String Priority level of the case.
ExabeamPlatform.Case.products String Products involved in the case.
ExabeamPlatform.Case.queue String Queue to which the case is assigned.
ExabeamPlatform.Case.riskScore Number Risk score of the case.
ExabeamPlatform.Case.rules.approxLogTime Number Approximate log time of the rule that triggered the case.
ExabeamPlatform.Case.rules.ruleId String Unique identifier for the rule.
ExabeamPlatform.Case.rules.ruleName String Name of the rule that triggered the case.
ExabeamPlatform.Case.rules.ruleReason String Reason for the rule triggering the case.
ExabeamPlatform.Case.rules.ruleSeverity String Severity level of the rule.
ExabeamPlatform.Case.rules.ruleSource String Source of the rule.
ExabeamPlatform.Case.rules.ruleType String Type of the rule.
ExabeamPlatform.Case.srcHosts Unknown Source hosts involved in the case.
ExabeamPlatform.Case.srcIps Unknown Source IP addresses involved in the case.
ExabeamPlatform.Case.stage String Current stage of the case.
ExabeamPlatform.Case.subscriptionCode String Subscription code associated with the case.
ExabeamPlatform.Case.tags Unknown Tags associated with the case.
ExabeamPlatform.Case.useCases Unknown Use cases associated with the case.
ExabeamPlatform.Case.users Unknown Users involved in the case.
ExabeamPlatform.Case.vendors String Vendors involved in the case.
ExabeamPlatform.Case.alertCreationTimestamp Date Timestamp when the alert was created.
ExabeamPlatform.Case.alertDescriptionRt String Description of the alert.
ExabeamPlatform.Case.creationBy String User who created the case.
ExabeamPlatform.Case.creationTimestamp Date Timestamp when the case was created.
ExabeamPlatform.Case.destEndpoints Unknown Destination endpoints involved in the case.
ExabeamPlatform.Case.mitres.tacticKey String Key of the MITRE tactic associated with the case.
ExabeamPlatform.Case.mitres.technique String MITRE technique associated with the case.
ExabeamPlatform.Case.mitres.techniqueKey String Key of the MITRE technique associated with the case.

exabeam-platform-get-events


Get cases from Exabeam Security Operations Platform as Cortex XSIAM events. This command is supported in Cortex XSIAM only and is intended to be used for debugging purposes as it may result in duplicate events.

Base Command

exabeam-platform-get-events

Input

Argument Name Description Required
start_time The starting date for the case search range. For example: yyyy-MM-ddThh:mm:ssZ. Default is 1 hour ago. Optional
end_time The ending date for the case search range. For example: yyyy-MM-ddThh:mm:ssZ. Default is now. Optional
limit The maximum number of results to return. Default is 10. Optional
should_push_events If true, the command will push the events to the Cortex XSIAM dataset. Otherwise, it will only display them. Default is false. Optional

Context Output

There is no context output for this command.

Command example

!exabeam-platform-get-events start_time="2025-08-30T00:27:53Z" limit=1 should_push_events=false

Human Readable Output

Events

_time alertId approxLogTime caseCreationTimestamp caseId caseNumber destHost destIp hasAttachments lastModifiedTimestamp mitres name priority product queue riskScore rules srcHost srcIp stage subscriptionCode tags useCases user vendor
2025-08-31T22:49:43Z 8ccd4479-aaaa-bbbb-9a29-aaed1b7e4d69 1756658220000000 1756680583393478 573d2e67-aaaa-bbbb-1122-11998166047e 1449     false 1756724116013483   Hello_world_rule HIGH Correlation Rule Tier 1 Analyst 200 [ ]     NEW 1234       Exabeam

exabeam-get-threat-summary


Retrieve the Copilot Threat Summary for an individual alert.

Base Command

exabeam-get-threat-summary

Input

Argument Name Description Required
alert_id The alertId to fetch a threat summary for. Required

Context Output

Path Type Description
ExabeamPlatform.Alert.Summary string Threat Summary of the Exabeam Alert.

exabeam-update-case-details


Update details for a specific case.

Base Command

exabeam-update-case-details

Input

Argument Name Description Required
case_id ID of the case to be updated. Required
alert_description Alert description in either plaintext or HTML format. Length <=1024. Optional
alert_name Alert name. Length <= 128. Optional
priority Alert priority. Possible values are: CRITICAL, HIGH, MEDIUM, LOW. Optional
stage Case stage. Possible values are: NEW, INVESTIGATION, REMEDIATION, CLOSED. Optional
closed_reason Reason for case closure. Possible values are: Already Mitigated or Resolved, False Positive or Duplicate, Low Risk, Rule Misconfiguration, Policy or Setup Issue, Other. Optional
supporting_reason Supporting reason for case closure. Optional
assignee Assignee for the case. Optional
queue Queue for the case. Optional

Context Output

There is no context output for this command.

exabeam-platform-list-case-notes


Retrieve a list of notes associated with the specified caseId.

Base Command

exabeam-platform-list-case-notes

Input

Argument Name Description Required
case_id Unique ID that identifies a case. Required

Context Output

There is no context output for this command.

exabeam-platform-create-case-note


Add a new note to the specified case.

Base Command

exabeam-platform-create-case-note

Input

Argument Name Description Required
case_id Unique ID that identifies a case. Required
note Note text. Required

Context Output

There is no context output for this command.

<~PLATFORM>

License Requirements

The following configuration parameters require the Cortex XSIAM license:

  • Fetch events

</~PLATFORM>

Configuration parameters

  • url — Server URL (required)
  • credentials — Client ID (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • isFetchEvents — Fetch events
  • incidentFetchInterval — Incidents Fetch Interval
  • eventFetchInterval — Events Fetch Interval
  • max_fetch — Maximum Incidents Per Fetch
  • max_events_fetch — Maximum Number of Cases Per Fetch
  • fetch_query — Fetch query
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • incidentType — Incident type

Commands (12)

  • exabeam-get-threat-summary

    Retrieve the Copilot Threat Summary for an individual alert.

  • exabeam-platform-alert-search

    Search for alerts that match one or more search criteria.

  • exabeam-platform-case-search

    Search for cases that match one or more search criteria. For example, you can search for cases that are associated with a specific case ID and that reference specific rules.

  • exabeam-platform-context-table-delete

    Delete a specific context table, including records and attributes.

  • exabeam-platform-context-table-list

    Retrieve metadata for all existing context tables, including source, operational status, and attribute mapping.

  • exabeam-platform-create-case-note

    Add a new note to the specified case.

  • exabeam-platform-event-search

    Get events from Exabeam Security Operations Platform.

  • exabeam-platform-get-events

    Get cases from Exabeam Security Operations Platform as Cortex XSIAM events. This command is supported in Cortex XSIAM only and is intended to be used for debugging purposes as it may result in duplicate events.

  • exabeam-platform-list-case-notes

    Retrieve a list of notes associated with the specified caseId.

  • exabeam-platform-table-record-create

    Add one or more context records directly to an existing table.

  • exabeam-platform-table-record-list

    Retrieve the records for a specific context table.

  • exabeam-update-case-details

    Update details for a specific case.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
TOKEN_EXPIRY_BUFFER = timedelta(seconds=10)

# Fetch Incidents (XSOAR)
DEFAULT_LIMIT = 50
MAX_LIMIT = 3000

# Fetch Events (XSIAM & Platform)
MAX_BATCH_SIZE = 3000
FETCH_EVENTS_DEFAULT_LIMIT = 30000
VENDOR = "Exabeam"
PRODUCT = "Threat Center"

# Get events (XSIAM & Platform)
GET_EVENTS_DEFAULT_LIMIT = 10
GET_EVENTS_DEFAULT_FROM_DATE = "1 hour ago"
GET_EVENTS_DEFAULT_TO_DATE = "now"


""" CLIENT CLASS """


class Client(BaseClient):
    """
    Exabeam Client: A Python Wrapper for Interacting with the Exabeam API
    """

    def __init__(self, base_url: str, client_id: str, client_secret: str, verify: bool, proxy: bool):
        super().__init__(base_url=f"{base_url}", verify=verify, proxy=proxy, timeout=20)
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token = None

        self._authenticate()

    def _authenticate(self):
        """
        Authenticates to the Exabeam API using the provided client_id and client_password.
        This function must be called before any other API calls.
        Note: the session is automatically closed in BaseClient's __del__
        """
        integration_context = demisto.getIntegrationContext()
        access_token = integration_context.get("access_token")
        expiry_time_str = integration_context.get("expiry_time_utc")

        if self._is_token_valid(access_token, expiry_time_str):
            self.access_token = access_token
        else:
            self._get_new_token()

    def _is_token_valid(self, access_token, expiry_time_str):
        """
        Checks if the current token is valid and not expired with a security buffer.
        """
        if not access_token or not expiry_time_str:
            return False

        current_time_utc = datetime.now(timezone.utc)
        expiry_time_utc = datetime.fromisoformat(expiry_time_str)
        return current_time_utc < (expiry_time_utc - TOKEN_EXPIRY_BUFFER)

    def _get_new_token(self):
        """
        Fetches a new token from the Exabeam API and updates the integration context.
        """
        data = {
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "grant_type": "client_credentials",
        }

        response = self._http_request(
            method="POST",
            full_url=f"{self._base_url}/auth/v1/token",
            data=data,
        )

        new_token = response.get("access_token")
        expires_in = response.get("expires_in")
        current_time_utc = datetime.now(timezone.utc)
        expiry_time_utc = current_time_utc + timedelta(seconds=expires_in)

        demisto.setIntegrationContext({"access_token": new_token, "expiry_time_utc": expiry_time_utc.isoformat()})
        self.access_token = new_token

    def request(self, **kargs):
        """
        Executes an HTTP request with automatic token refresh on expiration.

        This method sets the required headers, including the Authorization token,
        and performs the HTTP request using `_http_request`. If the request fails
        due to an expired JWT token, the token is refreshed and the request is retried.

        Args:
            **kargs: Arbitrary keyword arguments passed to `_http_request`,
                    such as method, url, data, params, etc.

        Returns:
            Any: The response from the HTTP request, typically a JSON object.

        Raises:
            DemistoException: If the response contains an error message or if
                            the request fails due to reasons other than token expiration.
        """
        kargs["headers"] = {
            "accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.access_token}",
        }

        def _make_request() -> Any:
            response = self._http_request(**kargs)
            if isinstance(response, dict) and (error := response.get("errors", {})):
                raise DemistoException(error.get("message"))
            return response

        try:
            return _make_request()
        except DemistoException as e:
            if (
                hasattr(e, "res")
                and hasattr(e.res, "status_code")
                and e.res.status_code == 401  # type: ignore
                and "Jwt is expired" in e.res.text  # type: ignore
            ):
                self._get_new_token()
                kargs["headers"]["Authorization"] = f"Bearer {self.access_token}"
                return _make_request()
            else:
                raise

    def event_search_request(self, data_dict: dict) -> dict:
        """
        Performs basic get request to check if the server is reachable.
        """
        data = json.dumps(data_dict)
        full_url = f"{self._base_url}/search/v2/events"
        response = self.request(
            method="POST",
            full_url=full_url,
            data=data,
        )
        return response

    def case_search_request(self, data_dict: dict) -> dict:
        """
        Searches for cases in the threat center.
        """
        data = json.dumps(data_dict)
        full_url = f"{self._base_url}/threat-center/v1/search/cases"
        response = self.request(method="POST", full_url=full_url, data=data)
        return response

    def get_case_request(self, case_id: int) -> dict:
        """
        Retrieves details of a specific case by its ID.
        """
        full_url = f"{self._base_url}/threat-center/v1/cases/{case_id}"
        response = self.request(method="GET", full_url=full_url)
        return response

    def alert_search_request(self, data_dict: dict) -> dict:
        """
        Searches for alerts in the threat center.
        """
        data = json.dumps(data_dict)
        full_url = f"{self._base_url}/threat-center/v1/search/alerts"
        response = self.request(
            method="POST",
            full_url=full_url,
            data=data,
        )
        return response

    def create_table_record(self, table_id, json_data):
        """
        Creates a new record in the specified table.
        """
        full_url = f"{self._base_url}/context-management/v1/tables/{table_id}/addRecords"
        response = self.request(method="POST", full_url=full_url, json_data=json_data)
        return response

    def check_tracker_id(self, tracker_id):
        """
        Checks the upload status of a tracker by its ID.
        """
        full_url = f"{self._base_url}/context-management/v1/tables/uploadStatus/{tracker_id}"
        response = self.request(method="GET", full_url=full_url)
        return response

    def list_context_table(self) -> dict:
        """
        Lists all context tables.
        """
        full_url = f"{self._base_url}/context-management/v1/tables"
        response = self.request(method="GET", full_url=full_url)
        return response

    def get_context_table(self, table_id) -> dict:
        """
        Retrieves details of a specific context table by its ID.
        """
        full_url = f"{self._base_url}/context-management/v1/tables/{table_id}"
        response = self.request(method="GET", full_url=full_url)
        return response

    def delete_context_table(self, table_id, params) -> dict:
        """
        Deletes a context table and optionally any unused custom attributes.
        """
        full_url = f"{self._base_url}/context-management/v1/tables/{table_id}"
        response = self.request(method="DELETE", full_url=full_url, params=params)
        return response

    def get_table_record_list(self, table_id, params) -> dict:
        """
        Retrieves a list of records from a specific table.
        """
        full_url = f"{self._base_url}/context-management/v1/tables/{table_id}/records"
        response = self.request(method="GET", full_url=full_url, params=params)
        return response

    def get_alert_request(self, alert_id: int) -> dict:
        """
        Retrieves details of a specific alert by its ID.
        """
        full_url = f"{self._base_url}/threat-center/v1/alerts/{alert_id}"
        response = self.request(method="GET", full_url=full_url)
        return response


""" HELPER FUNCTIONS """


def get_date(time: str, arg_name: str):
    """
    Get the date from a given time string.

    Args:
        time (str): The time string to extract the date from.

    Returns:
        str: The date extracted from the time string formatted in ISO 8601 format (YYYY-MM-DD),
        or None if the time string is invalid.
    """
    date_time = arg_to_datetime(arg=time, arg_name=arg_name, required=True)
    if not date_time:
        raise DemistoException(f"There was an issue parsing the {arg_name} provided.")
    date = date_time.strftime(DATE_FORMAT)
    return date


def transform_string(input_str: str) -> str:
    """
    Transform the input string into a formatted string.

    Args:
        input_str (str): The input string to be transformed. It should be in the format "key:value".

    Returns:
        str: The transformed string where the value part is converted to lowercase if it's "true" or "false",
        otherwise it's enclosed in double quotes.

    Examples:
        transform_string("status:true") -> 'status:true'
        transform_string("message:Hello World") -> 'message:"Hello World"'
    """
    if ":" not in input_str:
        return input_str
    key, value = input_str.split(":", 1)
    value = value.strip()
    if value.lower() in ["true", "false"]:
        return f"{key}:{value.lower()}"
    else:
        return f'{key}:"{value}"'


def process_string(input_str: str) -> str:
    """
    Process the input string by splitting it based on logical operators and transforming each part.

    Args:
        input_str: The input string to be processed. It may contain logical operators such as 'AND', 'OR', 'NOT', 'TO'.

    Returns:
        str: The processed string where each part is transformed using the transform_string function.
    """
    # Use word boundaries to match operators only as standalone words, not as substrings
    pattern = r"\b(AND|OR|NOT|TO)\b"
    logical_operators = {"AND", "OR", "NOT", "TO"}

    # Split the input string by logical operators while keeping the operators
    parts = re.split(pattern, input_str)

    transformed_parts = []
    for part in parts:
        part = part.strip()
        if not part:
            continue
        # Check if the part is a logical operator using set membership (O(1) lookup)
        if part in logical_operators:
            transformed_parts.append(part)
        else:
            # Transform non-operator parts
            transformed_parts.append(transform_string(part))

    return " ".join(transformed_parts)


def _parse_entry(entry: dict, fields_to_filter: list[str] = None):  # type: ignore
    """
    Parse a single entry from the API response into a dictionary, optionally filtering specific fields.

    Args:
        entry (dict): The entry from the API response.
        fields_to_filter (list[str], optional): A list of field names to include in the returned dictionary.
            If None, all fields are included.

    Returns:
        dict: The parsed entry dictionary, filtered by fields_to_filter if provided, or all fields if not.
    """

    if fields_to_filter:
        entry = {key: value for key, value in entry.items() if key in fields_to_filter}

    parsed = {
        "Id": entry.get("id"),
        "Raw Log Ids": entry.get("rawLogIds"),
        "Tier": entry.get("tier"),
        "Is Parsed": entry.get("parsed"),
        "Raw Logs": entry.get("rawLogs"),
        "Time": entry.get("time"),
        "Products": entry.get("products"),
        "Src Hosts": entry.get("srcHosts"),
        "Subscription Code": entry.get("subscriptionCode"),
        "Dest Hosts": entry.get("destHosts"),
        "Alert Name": entry.get("alertName"),
        "Case ID": entry.get("caseId"),
        "Src IPs": entry.get("srcIps"),
        "Alert ID": entry.get("alertId"),
        "Risk Score": entry.get("riskScore"),
        "Has Attachments": entry.get("hasAttachments"),
        "Vendors": entry.get("vendors"),
        "Grouped by Key": entry.get("groupedbyKey"),
        "Case Creation Timestamp": entry.get("caseCreationTimestamp"),
        "Priority": entry.get("priority"),
        "Last Modified Timestamp": entry.get("lastModifiedTimestamp"),
        "Tags": entry.get("tags"),
        "Stage": entry.get("stage"),
        "Dest IPs": entry.get("destIps"),
        "Queue": entry.get("queue"),
        "Name": entry.get("name"),
        "Source": entry.get("source"),
        "Context Type": entry.get("contextType"),
        "# Items": entry.get("totalItems"),
        "Status": entry.get("status"),
        "Last Updated": entry.get("lastUpdated"),
        "Rules": len(entry.get("rules", [])) if isinstance(entry.get("rules"), list) else None,
        "Mitre Ttps": len(entry.get("mitres", [])) if isinstance(entry.get("mitres"), list) else None,
        "Use Cases": len(entry.get("useCases", [])) if isinstance(entry.get("useCases"), list) else None,
        "users": len(entry.get("users", [])) if isinstance(entry.get("users"), list) else None,
    }

    final = remove_empty_elements(parsed)
    return final if final else None


def _parse_group_by(entry: dict, titles: list):
    """
    Parses a single entry from the API response into a dictionary based on provided titles.

    Args:
        entry (dict): The entry from the API response.
        titles (list): A list of keys to extract from the entry.

    Returns:
        dict or None: The parsed entry dictionary with non-empty elements or None if all elements are empty.
    """
    parsed = {}
    for title in titles:
        parsed.update({title: entry.get(title)})
    final = remove_empty_elements(parsed)
    return final if final else None


def get_limit(args: dict) -> int:
    """
    Get the limit value specified in the arguments.

    Args:
        args: A dictionary containing the 'limit' argument.

    Returns:
        int: The limit value if specified and less than or equal to 3000; otherwise, returns 3000 as the maximum limit.
        If the 'limit' argument is not present in the dictionary or is None, returns 50 as the default limit.
    """
    if limit := arg_to_number(args.get("limit")):
        return min(int(limit), MAX_LIMIT)

    return DEFAULT_LIMIT


def error_fixes(error: str):
    new_error = ""
    if "not enough values to unpack" in error:
        new_error = (
            "Recommendation:\nValidate the query argument against the syntax documentation in the integration description."
        )

    return new_error


def transform_dicts(input_dict: Dict[str, List[str]]) -> List[Dict[str, str]]:
    """
    Transforms a dictionary of lists into a list of dictionaries.

    This function takes a dictionary where each key is associated with a list of values and transforms it
    into a list of dictionaries, where each dictionary contains the corresponding elements from each list.

    Args:
        input_dict (Dict[str, List[str]]): The input dictionary where each key maps to a list of values.
                                           All lists must be of the same length.

    Returns:
        List[Dict[str, str]]: A list of dictionaries where each dictionary is constructed by taking the i-th element
                              from each list in the input dictionary.
    """
    # Checking that the lists are equal in length
    lengths = {len(v) for v in input_dict.values()}
    if len(lengths) > 1:
        raise DemistoException("All lists in the attributes must have the same length")

    length = next(iter(lengths))
    keys = list(input_dict.keys())

    result = []
    for i in range(length):
        entry = {key: input_dict[key][i] for key in keys}
        result.append(entry)

    return result


def convert_all_timestamp_to_datestring(incident: dict, key_suffix: str = "") -> dict:
    """
    Converts specified timestamp fields in an incident dictionary to date strings.

    Args:
        incident (dict): A dictionary containing incident data with timestamp fields.
        key_suffix (str): An optional key suffix. Defaults to an empty string.

    Returns:
        dict: The incident dictionary with timestamp fields converted to date strings.
    """
    keys = [
        "caseCreationTimestamp",
        "lastModifiedTimestamp",
        "creationTimestamp",
        "ingestTimestamp",
        "approxLogTime",
        "lastUpdated",
    ]
    for key in keys:
        if key in incident:
            incident[f"{key}{key_suffix}"] = timestamp_to_datestring(incident[key] / 1000, date_format=DATE_FORMAT)
    return incident


def get_cases_in_batches(
    client: Client,
    start_time: str,
    end_time: str,
    last_fetched_ids: list[str],
    max_fetch: int,
) -> tuple[list[dict], str, list[str]]:
    """
    Gets cases up to `max_fetch` in batches of up to `MAX_BATCH_SIZE` between the `start_time` and `end_time`

    Args:
        client (Client): API client instance.
        start_time (str): The starting date and time for searching cases in `DATE_FORMAT`.
        end_time (str): The end date and time for searching cases in `DATE_FORMAT`.
        last_fetched_ids (list[str]): The list of existing case IDs to check against.
        max_fetch (int): The maximum number of unique fetched cases.

    Returns:
        tuple[list[dict], str, list[str]]: Unique fetched cases, new start time, and last fetched case IDs.
    """
    all_cases: list[dict] = []
    all_fetched_ids = set(last_fetched_ids)
    iteration = 1

    while len(all_cases) < max_fetch:
        filter = " AND ".join(f'NOT caseId:"{case_id}"' for case_id in last_fetched_ids)
        request_body = {
            "limit": MAX_BATCH_SIZE,
            "filter": filter,
            "fields": ["*"],
            "orderBy": ["caseCreationTimestamp ASC"],
            "startTime": start_time,
            "endTime": end_time,
        }
        demisto.debug(f"Starting {iteration=}. Searching cases using {request_body=}.")
        response = client.case_search_request(request_body)

        batch_rows = response.get("rows", [])
        if not batch_rows:  # Empty batch indicates steam of cases has ended
            demisto.debug("Reached the end after getting empty batch. Stopping search for cases.")
            break

        unique_batch_cases: list[dict] = []  # Deduplicated and formatted cases
        for row in batch_rows:
            case_id = row.get("caseId")
            if case_id in all_fetched_ids:
                demisto.debug(f"Skipping duplicate row with {case_id=}.")
                continue

            all_fetched_ids.add(case_id)
            # Format case and add to list of cases
            row["_time"] = timestamp_to_datestring(row["caseCreationTimestamp"] / 1000, date_format=DATE_FORMAT)
            unique_batch_cases.append(row)
            all_cases.append(row)

            if len(all_cases) == max_fetch:
                demisto.debug(f"Reached the desired {max_fetch=}. Stopping iterating over batch rows.")
                break

        if not unique_batch_cases:
            demisto.debug("No new unique cases in this batch. Stopping search for cases.")
            break

        start_time, last_fetched_ids = get_last_case_time_and_ids(unique_batch_cases)

        if len(batch_rows) < MAX_BATCH_SIZE:  # Partial batch indicates steam of cases has ended
            demisto.debug(f"Got partial batch with {len(batch_rows)} rows. Finishing searching for cases.")
            break

        demisto.debug(f"Finished {iteration=}. Got {len(all_cases)} cases so far. New {start_time=} and {last_fetched_ids=}.")
        iteration += 1

    return all_cases, start_time, last_fetched_ids


def filter_existing_cases(cases: list[dict], ids_exists: list[str]) -> list:
    """
    Filters out cases that already exist in the provided list of existing IDs.

    Args:
        cases (list[dict]): A list of case dictionaries to be filtered. Each dictionary should contain at least a "caseId" key.
        ids_exists (list[str]): A list of existing case IDs to check against.

    Returns:
        list[dict]: A list of case dictionaries that do not have IDs present in the `ids_exists` list.
    """
    if ids_exists:
        demisto.debug(f"Existing IDs in last_run: {ids_exists}")

        filtered_cases = []
        for case in cases:
            case_id = case.get("caseId")
            if case_id not in ids_exists:
                filtered_cases.append(case)
            else:
                demisto.debug(f"Case with ID {case_id} already exists, skipping.")
        demisto.debug(f"After filtered cases count: {len(filtered_cases)}")
    else:
        filtered_cases = cases
    return filtered_cases


def filter_existing_cases_lr(cases: list[dict], ids_exists: list[str], last_run: str) -> list:
    if ids_exists:
        demisto.debug(f"Existing IDs in last_run: {ids_exists}")

        filtered_cases = []
        for case in cases:
            case_id = case.get("caseId")
            if case_id not in ids_exists:
                filtered_cases.append(case)
            else:
                case_creation_timestamp = timestamp_to_datestring(
                    case.get("caseCreationTimestamp", 0) / 1000, date_format=DATE_FORMAT
                )
                if case_creation_timestamp == last_run:
                    filtered_cases.append(case)
                else:
                    demisto.debug(f"Case with ID {case_id} already exists, skipping.")
        demisto.debug(f"After filtered cases count: {len(filtered_cases)}")
    else:
        filtered_cases = cases
    return filtered_cases


def get_last_case_time_and_ids(formatted_cases: list) -> tuple[str, list]:
    """
    Gets the maximum `_time` value from all formatted cases along with the IDs of cases with this `_time` value.

    Args:
        formatted_cases (list): A list of cases formatted as XSIAM events with `_time` value in the `DATE_FORMAT`.

    Raises:
        ValueError: If the list of cases is empty.

    Returns:
        tuple[str, list]: Maximum `_time` value, list of IDs of cases with this `_time` value.
    """
    if not formatted_cases:
        raise ValueError("Cannot get last case time and IDs from empty list.")

    last_case_time = max(case["_time"] for case in formatted_cases)
    last_case_ids = [case["caseId"] for case in formatted_cases if case["_time"] == last_case_time]

    return last_case_time, last_case_ids


def update_last_run(cases: list, end_time: str) -> dict:
    """
    Updates the last run time and list of case IDs based on the provided cases.

    Args:
        cases (list): A list of case dictionaries, each containing a 'caseCreationTimestamp' and 'caseId'.
        end_time (str): The end time to use if no cases are provided.

    Returns:
        dict: A dictionary with:
            - 'time': The latest case creation timestamp formatted as a date string (or end_time if no cases are provided).
            - 'last_ids': A list of case IDs where the 'caseCreationTimestamp' matches the latest timestamp exactly.
    """
    if cases:
        max_timestamp = max(case.get("caseCreationTimestamp", 0) for case in cases)
        max_time_in_format = timestamp_to_datestring(max_timestamp / 1000, date_format=DATE_FORMAT)
        list_ids = []
        for case in cases:
            case_time_in_format = timestamp_to_datestring(case.get("caseCreationTimestamp", 0) / 1000, date_format=DATE_FORMAT)
            if case_time_in_format == max_time_in_format:
                list_ids.append(case.get("caseId", ""))
        last_run_time = max_time_in_format
    else:
        last_run_time = end_time
        list_ids = []

    last_run = {"time": last_run_time, "last_ids": list_ids}
    return last_run


def format_incidents(cases: list[dict]) -> list[dict]:
    """
    Converts a list of cases into a list of incidents with formatted timestamps.

    Args:
        cases (list): A list of case dictionaries.

    Returns:
        list: A list of incident dictionaries, each containing:
            - 'Name': The alert name from the case.
            - 'rawJSON': The case data as a JSON string.
    """
    incidents = []
    for case in cases:
        case = convert_all_timestamp_to_datestring(case)
        alert_name = case.get("alertName", "")
        incidents.append(
            {
                "Name": alert_name,
                "rawJSON": json.dumps(case),
            }
        )
    return incidents


def format_record_keys(dict_list):
    new_list = []
    for input_dict in dict_list:
        new_dict = {}
        for key, value in input_dict.items():
            new_key = key.replace("_", " ").title()
            new_dict[new_key] = value
        new_list.append(new_dict)
    return new_list


""" COMMAND FUNCTIONS """


def event_search_command(client: Client, args: dict) -> CommandResults:
    """
    Search for logs using the Exabeam client with the provided arguments.

    Args:
        client: An instance of the Exabeam client used to make the search request.
        args: A dictionary containing search query parameters and options.

    Returns:
        CommandResults: A CommandResults object containing the search results in both structured and human-readable formats.
    """
    start_time = get_date(args.get("start_time", "7 days ago"), "start_time")
    end_time = get_date(args.get("end_time", "today"), "end_time")
    if start_time > end_time:
        raise DemistoException("Start time must be before end time.")

    kwargs = {
        "filter": process_string(args.get("query", "")),
        "fields": argToList(args.get("fields", "*")),
        "limit": get_limit(args),
        "startTime": start_time,
        "endTime": end_time,
    }
    group_by = args.get("group_by")
    if group_by:
        group_list = argToList(group_by)
        kwargs.update({"groupBy": group_list, "fields": group_list})

    response = client.event_search_request(kwargs)

    if error := response.get("errors", {}):
        raise DemistoException(error.get("message"))

    data_response = response.get("rows", {})

    human_readable = []
    for entry in data_response:
        if group_by:
            if parsed_entry := _parse_group_by(entry, group_list):
                human_readable.append(parsed_entry)
        elif parsed_entry := _parse_entry(entry):
            human_readable.append(parsed_entry)

    return CommandResults(
        outputs_prefix="ExabeamPlatform.Event",
        outputs=data_response,
        readable_output=tableToMarkdown(name="Logs", t=human_readable),
    )


def case_search_command(client: Client, args: dict) -> CommandResults:
    return generic_search_command(client, args, "case")


def alert_search_command(client: Client, args: dict) -> CommandResults:
    return generic_search_command(client, args, "alert")


def generic_search_command(client: Client, args: dict, item_type: str) -> CommandResults:
    """
    Searches for and retrieves items based on the provided item type and arguments.

    Args:
        client (Client): API client instance.
        args (dict): Search and filter parameters, including optional item IDs.
        item_type (str): Type of item to search for ('case' or 'alert').

    Returns:
        CommandResults: Contains search results and a Markdown table of the results.
    """
    if item_id := args.get(f"{item_type}_id"):
        if item_type == "case":
            data_response = [client.get_case_request(item_id)]
        elif item_type == "alert":
            data_response = [client.get_alert_request(item_id)]
        table_name = f"{item_type.capitalize()}"
    else:
        start_time = get_date(args.get("start_time", "7 days ago"), "start_time")
        end_time = get_date(args.get("end_time", "today"), "end_time")
        if start_time > end_time:
            raise DemistoException("The start time argument must be earlier than the end time.")
        kwargs = {
            "filter": process_string(args.get("query") or ""),
            "fields": argToList(args.get("fields", "*")),
            "startTime": start_time,
            "endTime": end_time,
        }
        all_results = argToBoolean(args.get("all_results", False))
        if not all_results:
            kwargs["limit"] = get_limit(args)
        if order_by := args.get("order_by", ""):
            kwargs["orderBy"] = argToList(order_by)

        if item_type == "case":
            response = client.case_search_request(kwargs)
        elif item_type == "alert":
            response = client.alert_search_request(kwargs)
        else:
            response = {}
            demisto.debug(f"{item_type=} -> {response=}")
        data_response = response.get("rows", [])
        table_name = f"{item_type.capitalize()}s"

    fields_to_human_readable = [
        "caseId",
        "alertId",
        "riskScore",
        "priority",
        "groupedbyValue",
        "groupedbyKey",
        "rules",
        "mitres",
        "useCases",
        "users",
        "stage",
        "queue",
    ]
    human_readable = [_parse_entry(row, fields_to_human_readable) for row in data_response]

    include_related_rules = argToBoolean(args.get("include_related_rules", False))
    if not include_related_rules:
        for row in data_response:
            row.pop("rules", None)

    return CommandResults(
        outputs_prefix=f"ExabeamPlatform.{item_type.capitalize()}",
        outputs=data_response,
        readable_output=tableToMarkdown(name=table_name, t=human_readable),
    )


def context_table_list_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves and returns context tables based on provided arguments.

    Args:
        client (Client): The client instance used for API requests.
        args (dict): A dictionary of arguments. May include:
            - 'table_id': ID of a specific context table to retrieve.
            - 'include_attributes': Boolean to determine if attributes should be included in the output.

    Returns:
        CommandResults: Contains:
            - outputs_prefix: Prefix for the output keys.
            - outputs: The raw data response from the API.
            - readable_output: A Markdown table of the context tables.
    """
    if table_id := args.get("table_id"):
        response = client.get_context_table(table_id)
        readable_output = _parse_entry(response)
        table_name = "Table"
    else:
        limit = get_limit(args)

        response = client.list_context_table()[:limit]

        include_attributes = argToBoolean(args.get("include_attributes"))

        readable_output = []
        for table in response:
            table = convert_all_timestamp_to_datestring(table)
            parsed_table = _parse_entry(table)
            readable_output.append(parsed_table)
            if not include_attributes:
                table.pop("attributes", None)
        table_name = "Tables"

    return CommandResults(
        outputs_prefix="ExabeamPlatform.ContextTable",
        outputs=response,
        readable_output=tableToMarkdown(name=table_name, t=readable_output),
    )


def context_table_delete_command(client: Client, args: dict) -> CommandResults:
    """
    Deletes a context table based on the provided table ID and optional parameters.

    Args:
        client (Client): The client instance used for API requests.
        args (dict): A dictionary of arguments. May include:
            - 'table_id': ID of the context table to delete.
            - 'delete_unused_custom_attributes': Boolean to specify if unused custom attributes should be deleted.

    Returns:
        CommandResults: Contains a readable message confirming the deletion of the context table.
    """
    table_id = args.get("table_id")
    include_attributes = argToBoolean(args.get("delete_unused_custom_attributes"))
    params = {"deleteUnusedCustomAttributes": str(include_attributes)}

    response = client.delete_context_table(table_id, params)
    table_id_response = response.get("id", None)

    return CommandResults(readable_output=f"The context table with ID {table_id_response} has been successfully deleted.")


def table_record_list_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves records from a specified table, with support for pagination.

    Args:
        client (Client): The client instance used for API requests.
        args (dict): A dictionary of arguments, including:
            - 'table_id' (str): ID of the table from which to retrieve records.
            - 'limit' (int, optional): Maximum number of records to retrieve. Defaults to a predefined limit.
            - 'page' (int, optional): The page number to retrieve. Defaults to 1 if 'page_size' is provided.
            - 'page_size' (int, optional): Number of records per page. Defaults to a predefined limit if 'page' is provided.

    Returns:
        CommandResults: The results of the command, including the records retrieved.
    """
    table_id = args.get("table_id")
    limit = arg_to_number(args.get("limit")) or DEFAULT_LIMIT
    page = arg_to_number(args.get("page"))
    page_size = arg_to_number(args.get("page_size"))
    records: list = []
    offset = 0

    if page:
        limit = min(page_size or DEFAULT_LIMIT, MAX_LIMIT)
        offset = (page - 1) * limit

    while len(records) < limit:
        params = {"limit": min(limit - len(records), MAX_LIMIT), "offset": offset}

        response = client.get_table_record_list(table_id, params)
        fetched_records = response.get("records", [])
        if not fetched_records:
            break

        records.extend(fetched_records)
        offset = len(records)

    readable_output = format_record_keys(records)

    return CommandResults(
        outputs_prefix="ExabeamPlatform.Record",
        outputs=records,
        readable_output=tableToMarkdown(name=f"Records of table id: {table_id}", t=readable_output),
    )


@polling_function(
    name="exabeam-platform-table-record-create",
    interval=arg_to_number(demisto.args().get("interval_in_seconds")),  # type: ignore
    timeout=arg_to_number(demisto.args().get("timeout")),  # type: ignore
    poll_message="Create records in process:",
    requires_polling_arg=False,
)
def table_record_create_command(args: dict, client: Client) -> PollResult:
    """
    Creates table records and polls their creation status.

    On first run, sends a request to create records and retrieves a `tracker_id`.
    On subsequent runs, checks the status using the `tracker_id`.

    Args:
        args (dict): Includes 'tracker_id', 'table_id', 'attributes', and 'operation'.
        client (Client): The client for API requests.

    Returns:
        PollResult: Contains the status and results of the record creation.
    """
    if not (tracker_id := args.get("tracker_id")):
        table_id = args.get("table_id")
        attributes = args.get("attributes", "")
        list_of_dict_attributes = transform_dicts(attributes)
        operation = args.get("operation")
        payload = {
            "operation": operation,
            "data": list_of_dict_attributes,
        }

        response = client.create_table_record(table_id, payload)
        tracker_id = response.get("trackerId", "")

    tracker_response = client.check_tracker_id(tracker_id)
    upload_status = tracker_response.get("uploadStatus")
    human_readable = {
        "Total Uploaded": tracker_response.get("totalUploaded"),
        "Total Errors": tracker_response.get("totalErrors"),
    }

    return PollResult(
        response=CommandResults(readable_output=tableToMarkdown("Completed", human_readable)),
        continue_to_poll=(upload_status != "completed"),
        args_for_next_run=args.update({"tracker_id": tracker_id}),
    )


def fetch_incidents(client: Client, params: dict[str, str], last_run) -> tuple[list, dict]:
    """
    Fetches incidents from the client based on specified parameters and updates the last run time.

    Args:
        client (Client): The client instance used for API requests.
        params (dict[str, str]): Dictionary of parameters for fetching incidents, including:
            - 'fetch_query': Filter query for incidents.
            - 'max_fetch': Maximum number of incidents to fetch.
            - 'first_fetch': Time range for the first fetch.
        last_run (dict): Last run data used to filter existing incidents.

    Returns:
        tuple[list, dict]:
            - A list of incidents.
            - Updated last run data.
    """
    demisto.debug(f"Last run before the fetch run: {last_run}")

    filter_query = params.get("fetch_query")
    limit = arg_to_number(params.get("max_fetch", DEFAULT_LIMIT)) or DEFAULT_LIMIT
    demisto.debug(f"Fetching incidents with limit={limit}")

    first_fetch = params.get("first_fetch", "3 days")
    start_time, end_time = get_fetch_run_time_range(last_run=last_run, first_fetch=first_fetch, date_format=DATE_FORMAT)
    demisto.debug(f"Fetching incidents between start_time={start_time} and end_time={end_time}")

    args = {
        "order_by": "caseCreationTimestamp",
        "query": filter_query,
        "start_time": start_time,
        "end_time": end_time,
        "limit": limit,
        "include_related_rules": True,
    }

    cases = case_search_command(client, args).outputs
    if not isinstance(cases, list):
        raise DemistoException("The response did not contain a list of cases.")
    demisto.debug(f"Response contain {len(cases)} cases")

    ids_exists = last_run.get("last_ids", [])
    cases_for_last_run = filter_existing_cases_lr(cases, ids_exists, start_time)
    cases_for_incidents = filter_existing_cases(cases, ids_exists)
    last_run = update_last_run(cases_for_last_run, end_time)
    demisto.debug(f"Last run after the fetch run: {last_run}")
    incidents = format_incidents(cases_for_incidents)
    demisto.debug(f"After the fetch incidents count: {len(incidents)}")
    return incidents, last_run


def fetch_events(client: Client, max_fetch: int, last_run: dict[str, Any]) -> tuple[list[dict], dict]:
    """
    Validates the `max_fetch` value, fetches Exabeam cases as XSIAM events in batches, and updates the last run.

    Args:
        client (Client): API client instance.
        max_fetch (int): The maximum number of cases to fetch as events.
        last_run (dict[str, Any]): Last run object from previous fetch.

    Returns:
        tuple[list[dict], dict]: List of cases formatted as events, updated last run object.
    """
    demisto.debug(f"Starting to fetch events with {max_fetch=}. Got {last_run=}.")

    start_time, end_time = get_fetch_run_time_range(last_run=last_run, first_fetch="1 minute ago", date_format=DATE_FORMAT)
    last_fetched_ids = last_run.get("last_ids", [])

    demisto.debug(f"Starting to fetch cases in batches with {start_time=}, {end_time=}, {last_fetched_ids=}.")
    events, new_start_time, new_last_fetched_ids = get_cases_in_batches(
        client=client,
        start_time=start_time,
        end_time=end_time,
        last_fetched_ids=last_fetched_ids,
        max_fetch=max_fetch,
    )

    next_run = {"time": new_start_time, "last_ids": new_last_fetched_ids}
    demisto.debug(f"Fetched {len(events)} cases in batches. Updated {next_run=}.")

    return events, next_run


def get_events_command(client: Client, args: dict[str, Any]) -> tuple[list[dict], CommandResults]:
    """
    Implements `exabeam-platform-get-events`; gets Exabeam cases as XSIAM events in batches.

    Args:
        client (Client): API client instance.
        args (dict[str, Any]): The command arguments.

    Returns:
        tuple[list[dict], CommandResults]: The events and the command results containing a human-readable table of events.
    """
    demisto.debug(f"Starting to get events with {args=}.")
    # `arg_to_datetime` does not return `None` here due to default. Added `type: ignore` to silence type checkers and linters
    start_time = arg_to_datetime(args.get("start_time", GET_EVENTS_DEFAULT_FROM_DATE)).strftime(DATE_FORMAT)  # type: ignore [union-attr]
    end_time = arg_to_datetime(args.get("end_time", GET_EVENTS_DEFAULT_TO_DATE)).strftime(DATE_FORMAT)  # type: ignore [union-attr]
    limit = arg_to_number(args.get("limit")) or GET_EVENTS_DEFAULT_LIMIT

    demisto.debug(f"Starting to get cases in batches with {start_time=}, {end_time=}, {limit=}.")
    events, *_ = get_cases_in_batches(
        client=client,
        start_time=start_time,
        end_time=end_time,
        last_fetched_ids=[],
        max_fetch=limit,
    )

    return events, CommandResults(readable_output=tableToMarkdown("Events", events))


def test_module(client: Client, params: dict[str, Any]) -> str:  # pragma: no cover
    """test function

    Args:
        client: Client

    Returns:
        'ok' if successful
        If we've reached this point, it indicates that the login process was successful.

    """
    if client.access_token and generic_search_command(client, {}, "case"):
        if params.get("isFetchEvents") and (is_xsiam() or is_platform()):
            fetch_events(client, max_fetch=1, last_run={})
        if params.get("isFetch") and is_xsoar():
            fetch_incidents(client, params, last_run={})
        return "ok"
    else:
        raise DemistoException("Access Token Generation Failure.")


def get_threat_summary(client: Client, args: dict) -> CommandResults:
    """
    Implements `exabeam-get-threat-summary`; gets Exabeam Threat Summary for a given ID

    Args:
        client (Client): API client instance.
        args (dict[str, Any]): The command arguments.

    Returns:
        CommandResults: Command results containing a human-readable threat summary.
    """
    data = json.dumps({"alertId": args.get("alert_id")})
    full_url = f"{client._base_url}/threat-center/v1/alerts/threat-explainer/prompt"
    response = client.request(method="POST", full_url=full_url, data=data, timeout=60)

    data_response = response.get("message")
    return CommandResults(
        outputs_prefix="ExabeamPlatform.Alert.Summary", outputs_key_field="", outputs=data_response, readable_output=data_response
    )


def update_case_details(client: Client, args: dict) -> CommandResults:
    """
    Implements `exabeam-update-case-details`; Update details for a specific case, as identified by case ID.

    Args:
        client (Client): API client instance.
        args (dict[str, Any]): The command arguments.

    Returns:
        CommandResults: Command results containing human-readable case details.
    """
    caseId = args.pop("case_id")
    newargs = {
        "alertDesciption": args.get("alert_desciption"),
        "alertName": args.get("alert_name"),
        "priority": args.get("priority"),
        "stage": args.get("stage"),
        "closedReason": args.get("closed_reason"),
        "supportingReason": args.get("supporting_reason"),
        "assignee": args.get("assignee"),
        "queue": args.get("queue"),
    }

    if newargs["stage"] == "CLOSED" and newargs["closedReason"] is None:
        demisto.error("A 'Closed Reason' must be provided when setting 'Stage' to 'CLOSED'")
        return_error("A 'Closed Reason' must be provided when setting 'Stage' to 'CLOSED'")

    request_data = json.dumps(newargs)
    full_url = f"{client._base_url}/threat-center/v2/cases/{caseId}"
    response = client.request(method="POST", full_url=full_url, data=request_data)

    return CommandResults(
        outputs_prefix="ExabeamPlatform.Event",
        outputs=response,
        readable_output=tableToMarkdown(name="Case ID: " + caseId, t=response),
    )


def list_case_notes(client: Client, args: dict) -> CommandResults:
    """
    Implements `exabeam-platform-list-case-notes`; Retrieve a list of notes associated with the specified caseId.

    Args:
        client (Client): API client instance.
        args (dict[str, Any]): The command arguments.

    Returns:
        CommandResults: Command results containing case notes
    """
    caseId = args.pop("case_id")
    full_url = f"{client._base_url}/threat-center/v1/cases/{caseId}/notes"
    response = client.request(method="GET", full_url=full_url)
    keys_to_remove = ["case_id", "is_deleted", "is_edited", "last_modified_timestamp", "text_rt"]
    filtered_response = []

    for i in response:
        filtered_response.append({key: value for key, value in i.items() if key not in keys_to_remove})

    return CommandResults(
        outputs_prefix="ExabeamPlatform.Notes",
        outputs=response,
        readable_output=tableToMarkdown(name="Case Notes:" + caseId, t=filtered_response),
    )


def create_case_note(client: Client, args: dict) -> CommandResults:
    """
    Implements `exabeam-platform-create-case-note`; Add a new note to the specified case.

    Args:
        client (Client): API client instance.
        args (dict[str, Any]): The command arguments.

    Returns:
        CommandResults: Command results showing the new case note
    """
    caseId = args.pop("case_id")
    request_data = json.dumps(args)
    full_url = f"{client._base_url}/threat-center/v1/cases/{caseId}/notes"
    response = client.request(method="POST", full_url=full_url, data=request_data)

    return CommandResults(
        outputs_prefix="ExabeamPlatform.Notes",
        outputs=response,
        readable_output=tableToMarkdown(name="Case Notes:" + caseId, t=response[0]),
    )


""" MAIN FUNCTION """


def main() -> None:  # pragma: no cover
    params = demisto.params()
    args = demisto.args()
    command = demisto.command()
    credentials = params.get("credentials", {})
    client_id = credentials.get("identifier")
    client_secret = credentials.get("password")
    base_url = params.get("url", "")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    try:
        client = Client(
            base_url.rstrip("/"), verify=verify_certificate, client_id=client_id, client_secret=client_secret, proxy=proxy
        )

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

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

        elif command == "fetch-incidents" and is_xsoar():
            last_run = demisto.getLastRun()
            incidents, next_run = fetch_incidents(client, params, last_run)
            demisto.incidents(incidents)
            demisto.setLastRun(next_run)

        elif command == "fetch-events" and (is_xsiam() or is_platform()):
            max_fetch = arg_to_number(params.get("max_events_fetch", FETCH_EVENTS_DEFAULT_LIMIT)) or FETCH_EVENTS_DEFAULT_LIMIT
            last_run = demisto.getLastRun()
            events, next_run = fetch_events(client, max_fetch, last_run)
            send_events_to_xsiam(events, product=PRODUCT, vendor=VENDOR)
            demisto.setLastRun(next_run)

        elif command == "exabeam-platform-get-events" and (is_xsiam() or is_platform()):
            should_push_events = argToBoolean(args.pop("should_push_events", "false"))
            events, results = get_events_command(client, args)
            return_results(results)
            if should_push_events:
                send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)

        elif command == "exabeam-platform-event-search":
            return_results(event_search_command(client, args))
        elif command == "exabeam-platform-case-search":
            return_results(case_search_command(client, args))
        elif command == "exabeam-platform-alert-search":
            return_results(alert_search_command(client, args))
        elif command == "exabeam-platform-context-table-list":
            return_results(context_table_list_command(client, args))
        elif command == "exabeam-platform-context-table-delete":
            return_results(context_table_delete_command(client, args))
        elif command == "exabeam-platform-table-record-list":
            return_results(table_record_list_command(client, args))
        elif command == "exabeam-platform-table-record-create":
            return_results(table_record_create_command(args, client))
        elif command == "exabeam-get-threat-summary":
            return_results(get_threat_summary(client, args))
        elif command == "exabeam-update-case-details":
            return_results(update_case_details(client, args))
        elif command == "exabeam-platform-list-case-notes":
            return_results(list_case_notes(client, args))
        elif command == "exabeam-platform-create-case-note":
            return_results(create_case_note(client, args))
        else:
            raise NotImplementedError(f"Command {command} is not supported")

    except Exception as e:
        recommend = error_fixes(str(e))
        demisto.info(str(e))
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}\n{recommend}")


""" ENTRY POINT """


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