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 json
from datetime import datetime, timezone

import pytest
from pytest_mock import MockerFixture
from CommonServerPython import CommandResults, DemistoException
from ExabeamSecOpsPlatform import Client
from freezegun import freeze_time


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


class MockClient(Client):
    def __init__(self, base_url: str, username: str, password: str, verify: bool, proxy: bool):
        pass

    def event_search_command(self) -> None:
        return


def test_event_search_command_success(mocker: MockerFixture):
    """
    GIVEN:
        A mocked Exabeam client and valid search query arguments.

    WHEN:
        'event_search_command' function is called with the provided arguments.

    THEN:
        It should search for logs using the Exabeam client and return a CommandResults object containing
        the search results in both structured and human-readable formats.
    """
    from ExabeamSecOpsPlatform import event_search_command

    # Mock the response from the client's search_request method
    mock_response = {
        "rows": [
            {
                "id": "123",
                "rawLogIds": "1",
                "tier": "Tier",
                "parsed": "false",
                "rawLogs": "fictive",
                "time": "2024-01-30T11:20:07.000000+00:00",
                "message": "Log message 1",
                "activity": "trigger",
                "platform": "blackberry protect",
                "vendor": "BlackBerry",
            },
            {
                "id": "456",
                "time": "2024-01-30T11:21:06.976000+00:00",
                "message": "Log message 2",
                "activity": "trigger",
                "platform": "blackberry protect",
                "vendor": "BlackBerry",
            },
        ]
    }

    client = MockClient("", "", "", False, False)

    mocker.patch.object(client, "event_search_request", return_value=mock_response)

    # Define test arguments
    args = {
        "query": "",
        "fields": "message",
        "limit": "50",
        "start_time": "2024-05-01T00:00:00",
        "end_time": "2024-05-08T00:00:00",
    }

    # Call the event_search_command function
    response = event_search_command(client, args)

    assert isinstance(response, CommandResults)
    assert response.outputs_prefix == "ExabeamPlatform.Event"
    assert response.outputs == mock_response["rows"]
    expected_readable_output = (
        "### Logs\n"
        "|Id|Is Parsed|Raw Log Ids|Raw Logs|Tier|Time|\n"
        "|---|---|---|---|---|---|\n"
        "| 123 | false | 1 | fictive | Tier | 2024-01-30T11:20:07.000000+00:00 |\n"
        "| 456 |  |  |  |  | 2024-01-30T11:21:06.976000+00:00 |\n"
    )
    assert expected_readable_output in response.readable_output


def test_event_search_command_failure(mocker: MockerFixture):
    """
    GIVEN:
        A mocked Exabeam client and invalid search query arguments.

    WHEN:
        'event_search_command' function is called with invalid arguments.

    THEN:
        It should raise a DemistoException.
    """
    from ExabeamSecOpsPlatform import event_search_command

    # Mocking the client to simulate a response with errors
    client = MockClient("", "", "", False, False)
    mocker.patch.object(client, "event_search_request", return_value={"errors": {"message": "Error occurred"}})

    args = {
        "query": "",
        "fields": "message",
        "limit": "50",
        "start_time": "2024-05-01T00:00:00",
        "end_time": "2024-05-08T00:00:00",
    }

    with pytest.raises(DemistoException, match="Error occurred"):
        event_search_command(client, args)


def test_get_date(mocker: MockerFixture):
    """
    GIVEN:
        a mocked CommonServerPython.arg_to_datetime function returning a specific time string,

    WHEN:
        'get_date' function is called with the provided time string,

    THEN:
        it should return the date part of the provided time string in the 'YYYY-MM-DD' format.
    """
    from ExabeamSecOpsPlatform import get_date

    time = "2024.05.01T14:00:00"
    expected_result = "2024-05-01T14:00:00Z"

    with mocker.patch("CommonServerPython.arg_to_datetime", return_value=time):
        result = get_date(time, "start_time")

    assert result == expected_result


@pytest.mark.parametrize(
    "input_str, expected_output", [("key:Some Value", 'key:"Some Value"'), ("key:TrUe", "key:true"), ("key:false", "key:false")]
)
def test_transform_string(input_str, expected_output):
    """
    GIVEN:
        An input string to be transformed.
    WHEN:
        The 'transform_string' function is called with the input string.
    THEN:
        It should transform the input string according to the specified rules.
    """
    from ExabeamSecOpsPlatform import transform_string

    assert transform_string(input_str) == expected_output


@pytest.mark.parametrize(
    "input_str, expected_output",
    [
        ("key1:true AND key2:false OR key3:true TO key4:false", "key1:true AND key2:false OR key3:true TO key4:false"),
        ("key1:true", "key1:true"),
        ("", ""),
        ("key1:true AND key2:some value OR key3:another value", 'key1:true AND key2:"some value" OR key3:"another value"'),
        ("rules.rule_id:WTC-HT-TOW-A", 'rules.rule_id:"WTC-HT-TOW-A"'),
        ("hostname:TORONTO-SERVER", 'hostname:"TORONTO-SERVER"'),
        ("user:ACTOR-123", 'user:"ACTOR-123"'),
        ("device:AUTO-DEPLOY", 'device:"AUTO-DEPLOY"'),
        ("timestamp:* TO *", 'timestamp:"*" TO *'),
        ("value:100 TO value:200", 'value:"100" TO value:"200"'),
    ],
)
def test_process_string(input_str, expected_output):
    """
    GIVEN:
        An input string to be processed.
    WHEN:
        The 'process_string' function is called with the input string.
    THEN:
        It should correctly process the input string, splitting it based on logical operators and transforming each part using
        the 'transform_string' function.
    """
    from ExabeamSecOpsPlatform import process_string

    assert process_string(input_str) == expected_output


def test_event_search_request(mocker):
    """
    GIVEN:
        A dictionary containing data to be sent in the request.
        A mocked '_http_request' method of the YourClass class.
        A base URL and an access token.
    WHEN:
        The 'search_request' method of the YourClass class is called with the data dictionary.
    THEN:
        It should send a POST request to the specified URL with the provided data and headers.
    """
    mocker.patch("ExabeamSecOpsPlatform.Client._authenticate")
    mock_http_request = mocker.patch("ExabeamSecOpsPlatform.Client._http_request")
    base_url = "https://example-api.com"
    client_id = "your_client_id"
    client_secret = "your_client_secret"

    instance = Client(base_url=base_url, client_id=client_id, client_secret=client_secret, verify=False, proxy=False)
    instance.access_token = "dummy_token"
    data_dict = {"key": "value"}
    expected_url = "https://example-api.com/search/v2/events"
    expected_headers = {
        "Authorization": "Bearer dummy_token",
        "Content-Type": "application/json",
        "accept": "application/json",
    }
    mocked_response = {"response_key": "response_value"}
    mock_http_request.return_value = mocked_response
    result = instance.event_search_request(data_dict)

    mock_http_request.assert_called_once_with(
        method="POST",
        full_url=expected_url,
        data=json.dumps(data_dict),
        headers=expected_headers,
    )
    assert result == mocked_response


@pytest.mark.parametrize(
    "args, expected_output", [({}, 50), ({"limit": None}, 50), ({"limit": 1000}, 1000), ({"limit": 5000}, 3000)]
)
def test_get_limit(args, expected_output):
    """
    GIVEN:
        a dictionary containing the 'limit' argument with various values.

    WHEN:
        'get_limit' function is called with the provided dictionary.

    THEN:
        it should return the limit value if specified and less than or equal to 3000;
        otherwise, it should return 3000 as the maximum limit.
        If the 'limit' argument is not present in the dictionary or is None, it should return 50 as the default limit.
    """
    from ExabeamSecOpsPlatform import get_limit

    assert get_limit(args) == expected_output


def test_parse_group_by():
    """
    GIVEN:
        an entry dictionary containing information about an item with various attributes.

    WHEN:
        '_parse_group_by' function is called with the provided entry dictionary and a list of titles.

    THEN:
        it should return a parsed dictionary with non-empty elements based on the provided titles;
        empty elements should be removed.
    """
    from ExabeamSecOpsPlatform import _parse_group_by

    entry = {
        "Id": "123",
        "Vendor": "Vendor X",
        "Product": "",
        "Created_at": "2024-05-26T12:00:00",
        "Message": "This is a message.",
    }
    titles = ["Id", "Vendor", "Created_at", "Message"]
    expected_result = {"Id": "123", "Vendor": "Vendor X", "Created_at": "2024-05-26T12:00:00", "Message": "This is a message."}
    assert _parse_group_by(entry, titles) == expected_result


valid_expiry_time = (datetime(2024, 7, 23, 13, 0, tzinfo=timezone.utc)).isoformat()  # noqa: UP017
expired_expiry_time = (datetime(2024, 7, 23, 11, 0, tzinfo=timezone.utc)).isoformat()  # noqa: UP017


@pytest.mark.parametrize(
    "access_token, expiry_time_str, expected_result",
    [
        ("token", valid_expiry_time, True),
        ("token", expired_expiry_time, False),
        (None, valid_expiry_time, False),
        ("token", None, False),
    ],
)
@freeze_time("2024-07-23 12:00:00")
def test_is_token_valid(mocker: MockerFixture, access_token, expiry_time_str, expected_result):
    mocker.patch.object(Client, "_http_request", return_value={"access_token": "token", "expires_in": 0})
    client = Client(base_url="https://api.exabeam.com", client_id="abc123", client_secret="ABC123", verify=False, proxy=False)

    result = client._is_token_valid(access_token, expiry_time_str)
    assert result == expected_result


@pytest.mark.parametrize(
    "expected_response, expected_token",
    [
        (
            {"access_token": "token", "expires_in": 3600},
            "token",
        ),
    ],
)
def test_get_new_token(mocker: MockerFixture, expected_response, expected_token):
    http_request = mocker.patch.object(Client, "_http_request", return_value=expected_response)
    client = Client(base_url="https://api.exabeam.com", client_id="abc123", client_secret="ABC123", verify=False, proxy=False)

    client._get_new_token()
    http_request.assert_called_with(
        method="POST",
        full_url="https://api.exabeam.com/auth/v1/token",
        data={
            "client_id": "abc123",
            "client_secret": "ABC123",
            "grant_type": "client_credentials",
        },
    )


@pytest.mark.parametrize(
    "args, mock_response, expected_outputs, expected_readable_output",
    [
        (
            {
                "case_id": "123",
            },
            {
                "caseId": "123",
                "alertId": "456",
                "riskScore": 75,
                "groupedbyKey": "Src Ip",
                "srcIps": ["1.1.1.1"],
                "priority": "LOW",
                "stage": "NEW",
                "queue": "Tier 1 Analyst",
                "rules": [{"ruleSource": "CR"}],
            },
            [
                {
                    "caseId": "123",
                    "alertId": "456",
                    "riskScore": 75,
                    "groupedbyKey": "Src Ip",
                    "srcIps": ["1.1.1.1"],
                    "priority": "LOW",
                    "stage": "NEW",
                    "queue": "Tier 1 Analyst",
                }
            ],
            "### Case\n"
            "|Alert ID|Case ID|Grouped by Key|Priority|Queue|Risk Score|Rules|Stage|\n"
            "|---|---|---|---|---|---|---|---|\n"
            "| 456 | 123 | Src Ip | LOW | Tier 1 Analyst | 75 | 1 | NEW |\n",
        ),
        (
            {"limit": "1"},
            {
                "rows": [
                    {
                        "caseId": "123",
                        "alertId": "456",
                        "riskScore": 75,
                        "groupedbyKey": "Src Ip",
                        "srcIps": ["1.1.1.1"],
                        "priority": "LOW",
                        "stage": "NEW",
                        "queue": "Tier 1 Analyst",
                        "rules": [{"ruleSource": "CR"}],
                    }
                ],
                "totalRows": 1,
            },
            [
                {
                    "caseId": "123",
                    "alertId": "456",
                    "riskScore": 75,
                    "groupedbyKey": "Src Ip",
                    "srcIps": ["1.1.1.1"],
                    "priority": "LOW",
                    "stage": "NEW",
                    "queue": "Tier 1 Analyst",
                }
            ],
            "### Cases\n"
            "|Alert ID|Case ID|Grouped by Key|Priority|Queue|Risk Score|Rules|Stage|\n"
            "|---|---|---|---|---|---|---|---|\n"
            "| 456 | 123 | Src Ip | LOW | Tier 1 Analyst | 75 | 1 | NEW |\n",
        ),
    ],
)
def test_case_search_command(mocker: MockerFixture, args, mock_response, expected_outputs, expected_readable_output):
    from ExabeamSecOpsPlatform import case_search_command

    client = MockClient("", "", "", False, False)
    mocker.patch.object(client, "case_search_request", return_value=mock_response)
    mocker.patch.object(client, "get_case_request", return_value=mock_response)

    result = case_search_command(client, args)

    assert result.outputs_prefix == "ExabeamPlatform.Case"
    assert result.outputs == expected_outputs
    assert result.readable_output == expected_readable_output


def test_case_search_request(mocker: MockerFixture):
    data_dict = {
        "startTime": "2024-05-01T13:05:07.774Z",
        "endTime": "2024-06-21T13:05:07.774Z",
    }

    base_url = "https://example.com"
    client = Client(base_url, "", "", False, False)
    request = mocker.patch.object(client, "request", return_value={})

    client.case_search_request(data_dict)

    request.assert_called_with(
        method="POST",
        full_url=f"{base_url}/threat-center/v1/search/cases",
        data=json.dumps(data_dict),
    )


@pytest.mark.parametrize(
    "args, mock_response, expected_outputs, expected_readable_output",
    [
        (
            {"table_id": "123"},
            {"id": "123", "name": "Sample Table", "attributes": {"attr1": "value1"}},
            {"id": "123", "name": "Sample Table", "attributes": {"attr1": "value1"}},
            "### Table\n|Id|Name|\n|---|---|\n| 123 | Sample Table |\n",
        ),
        (
            {"limit": "1", "include_attributes": "Yes"},
            [
                {"id": "123", "name": "Table 1", "attributes": {"attr1": "value1"}},
                {"id": "124", "name": "Table 2", "attributes": {"attr2": "value2"}},
            ],
            [{"id": "123", "name": "Table 1", "attributes": {"attr1": "value1"}}],
            "### Tables\n|Id|Name|\n|---|---|\n| 123 | Table 1 |\n",
        ),
    ],
)
def test_context_table_list_command(mocker: MockerFixture, args, mock_response, expected_outputs, expected_readable_output):
    from ExabeamSecOpsPlatform import context_table_list_command

    client = MockClient("example.com", "", "", False, False)

    if "table_id" in args:
        mock_get = mocker.patch.object(client, "get_context_table", return_value=mock_response)
    else:
        mock_list = mocker.patch.object(client, "list_context_table", return_value=mock_response)

    result = context_table_list_command(client, args)

    if "table_id" in args:
        mock_get.assert_called_once_with(args["table_id"])
    else:
        mock_list.assert_called_once()

    assert result.outputs == expected_outputs
    assert result.readable_output == expected_readable_output


@pytest.mark.parametrize(
    "args, mock_response, expected_output",
    [
        (
            {"table_id": "12345", "delete_unused_custom_attributes": "True"},
            {"id": "1234"},
            "The context table with ID 1234 has been successfully deleted.",
        ),
        (
            {"table_id": "12345", "delete_unused_custom_attributes": "False"},
            {"id": "12345"},
            "The context table with ID 12345 has been successfully deleted.",
        ),
    ],
)
def test_context_table_delete_command(mocker: MockerFixture, args, mock_response, expected_output):
    from ExabeamSecOpsPlatform import context_table_delete_command

    client = MockClient("example.com", "", "", False, False)
    mock_delete = mocker.patch.object(client, "delete_context_table", return_value=mock_response)

    result = context_table_delete_command(client, args)

    assert result.readable_output == expected_output
    mock_delete.assert_called_once_with(
        args["table_id"], {"deleteUnusedCustomAttributes": str(args["delete_unused_custom_attributes"])}
    )


@pytest.mark.parametrize(
    "args, mock_response, expected_output",
    [
        (
            {"table_id": "12345", "limit": "2"},
            {"records": [{"id": "1", "name": "Record1"}, {"id": "2", "name": "Record2"}]},
            [
                {"id": "1", "name": "Record1"},
                {"id": "2", "name": "Record2"},
            ],
        )
    ],
)
def test_table_record_list_command(mocker: MockerFixture, args, mock_response, expected_output):
    from ExabeamSecOpsPlatform import table_record_list_command

    client = MockClient("example.com", "", "", False, False)
    mock_get = mocker.patch.object(client, "get_table_record_list", return_value=mock_response)

    result = table_record_list_command(client, args)

    assert result.outputs == expected_output
    mock_get.assert_called_once_with("12345", {"limit": 2, "offset": 0})


@pytest.mark.parametrize(
    "args, item_type, mock_response, expected_output, expected_prefix",
    [
        # with item_id
        (
            {"case_id": "123"},
            "case",
            {"id": "123", "name": "Test Case", "riskScore": "80"},
            [{"id": "123", "name": "Test Case", "riskScore": "80"}],
            "ExabeamPlatform.Case",
        ),
        (
            {"alert_id": "456"},
            "alert",
            {"id": "456", "name": "Test Alert", "riskScore": "80"},
            [{"id": "456", "name": "Test Alert", "riskScore": "80"}],
            "ExabeamPlatform.Alert",
        ),
        #  without item_id
        (
            {},
            "case",
            {"rows": [{"id": "123", "name": "Test Case", "riskScore": "80"}]},
            [{"id": "123", "name": "Test Case", "riskScore": "80"}],
            "ExabeamPlatform.Case",
        ),
    ],
)
def test_generic_search_command(mocker: MockerFixture, args, item_type, mock_response, expected_output, expected_prefix):
    from ExabeamSecOpsPlatform import generic_search_command

    client = MockClient("example.com", "", "", False, False)

    if f"{item_type}_id" in args:
        request = mocker.patch.object(client, f"get_{item_type}_request", return_value=mock_response)
    else:
        request = mocker.patch.object(client, f"{item_type}_search_request", return_value=mock_response)

    result = generic_search_command(client, args, item_type)

    assert result.outputs == expected_output
    assert result.outputs_prefix == expected_prefix

    if f"{item_type}_id" in args:
        request.assert_called_once_with(args[f"{item_type}_id"])
    else:
        request.assert_called_once()


@pytest.mark.parametrize(
    "dict_input, dict_expected",
    [
        (
            {"name": ["Alice", "Bob", "Charlie"], "age": ["25", "30", "35"], "city": ["city1", "city2", "city3"]},
            [
                {"name": "Alice", "age": "25", "city": "city1"},
                {"name": "Bob", "age": "30", "city": "city2"},
                {"name": "Charlie", "age": "35", "city": "city3"},
            ],
        ),
    ],
)
def test_transform_dicts(dict_input, dict_expected):
    from ExabeamSecOpsPlatform import transform_dicts

    result = transform_dicts(dict_input)
    assert result == dict_expected


@pytest.mark.parametrize(
    "attributes_input, key_suffix, expected_output",
    [
        pytest.param(
            {"caseCreationTimestamp": 1672531200000000, "lastModifiedTimestamp": 1672617600000000},
            "",
            {"caseCreationTimestamp": "2023-01-01T00:00:00Z", "lastModifiedTimestamp": "2023-01-02T00:00:00Z"},
            id="No suffix. Expect keys overwritten",
        ),
        pytest.param(
            {"approxLogTime": 1755685620000000},
            "Formatted",
            {"approxLogTime": 1755685620000000, "approxLogTimeFormatted": "2025-08-20T10:27:00Z"},
            id="'Formatted' suffix. Expect new keys added",
        ),
    ],
)
def test_convert_all_timestamp_to_datestring(attributes_input: dict, key_suffix: str, expected_output: dict):
    from ExabeamSecOpsPlatform import convert_all_timestamp_to_datestring

    result = convert_all_timestamp_to_datestring(attributes_input, key_suffix=key_suffix)
    assert result == expected_output


def test_last_case_time_and_ids():
    from ExabeamSecOpsPlatform import get_last_case_time_and_ids

    formatted_cases = [
        {"caseId": "A", "_time": "2025-01-01T00:00:00Z"},
        {"caseId": "B", "_time": "2025-01-01T01:01:00Z"},
        {"caseId": "C", "_time": "2025-01-01T01:01:00Z"},
    ]

    last_case_time, last_case_ids = get_last_case_time_and_ids(formatted_cases)

    assert last_case_time == "2025-01-01T01:01:00Z"
    assert last_case_ids == ["B", "C"]


@pytest.mark.parametrize(
    "mock_response, params, last_run, expected_incidents, expected_last_run",
    [
        pytest.param(
            CommandResults(
                outputs_prefix="ExabeamPlatform.Case",
                readable_output="",
                outputs=[
                    {
                        "caseId": "aa11",
                        "alertName": "alert1",
                        "caseCreationTimestamp": 1723212955501076,
                    },
                    {
                        "caseId": "bb22",
                        "alertName": "alert2",
                        "caseCreationTimestamp": 1723212955501077,
                    },
                ],
            ),
            {"fetch_query": "priority:LOW", "max_fetch": "2", "first_fetch": "3 days"},
            {"time": "2024-08-12T01:55:35Z", "last_ids": ["aa11"]},
            [
                {
                    "Name": "alert2",
                    "rawJSON": '{"caseId": "bb22", "alertName": "alert2", "caseCreationTimestamp": "2024-08-09T14:15:55Z"}',
                }
            ],
            {"time": "2024-08-09T14:15:55Z", "last_ids": ["bb22"]},
            id="different incident creation time",
        ),
        pytest.param(
            CommandResults(
                outputs_prefix="ExabeamPlatform.Case",
                readable_output="",
                outputs=[
                    {
                        "caseId": "aa11",
                        "alertName": "alert1",
                        "caseCreationTimestamp": 1723212955501077,
                    },
                    {
                        "caseId": "bb22",
                        "alertName": "alert2",
                        "caseCreationTimestamp": 1723212955501077,
                    },
                ],
            ),
            {"fetch_query": "priority:LOW", "max_fetch": "2", "first_fetch": "3 days"},
            {"time": "2024-08-09T14:15:55Z", "last_ids": ["aa11"]},
            [
                {
                    "Name": "alert2",
                    "rawJSON": '{"caseId": "bb22", "alertName": "alert2", "caseCreationTimestamp": "2024-08-09T14:15:55Z"}',
                },
            ],
            {"time": "2024-08-09T14:15:55Z", "last_ids": ["aa11", "bb22"]},
            id="same incident creation time, expected to add bb22 to last run and create incident just for bb22",
        ),
        pytest.param(
            CommandResults(
                outputs_prefix="ExabeamPlatform.Case",
                readable_output="",
                outputs=[
                    {
                        "caseId": "aa11",
                        "alertName": "alert1",
                        "caseCreationTimestamp": 1723212955501077,
                    }
                ],
            ),
            {"fetch_query": "priority:LOW", "max_fetch": "2", "first_fetch": "3 days"},
            {"time": "2024-08-09T14:15:55Z", "last_ids": ["aa11"]},
            [],
            {"time": "2024-08-09T14:15:55Z", "last_ids": ["aa11"]},
            id="same incident again, expected to keep it in last run and not create incident",
        ),
        pytest.param(
            CommandResults(
                outputs_prefix="ExabeamPlatform.Case",
                readable_output="",
                outputs=[{"caseId": "cc33", "alertName": "alert3", "caseCreationTimestamp": 1723213855501000}],
            ),
            {"fetch_query": "priority:LOW", "max_fetch": "2", "first_fetch": "3 days"},
            {"time": "2024-08-09T14:15:55Z", "last_ids": ["aa11", "bb22"]},
            [
                {
                    "Name": "alert3",
                    "rawJSON": '{"caseId": "cc33", "alertName": "alert3", "caseCreationTimestamp": "2024-08-09T14:30:55Z"}',
                },
            ],
            {"time": "2024-08-09T14:30:55Z", "last_ids": ["cc33"]},
            id="new incident, expected to create incident and update last run",
        ),
    ],
)
def test_fetch_incidents(mocker: MockerFixture, mock_response, params, last_run, expected_incidents, expected_last_run):
    from ExabeamSecOpsPlatform import fetch_incidents

    client = MockClient("example.com", "", "", False, False)

    mocker.patch("ExabeamSecOpsPlatform.case_search_command", return_value=mock_response)

    incidents, updated_last_run = fetch_incidents(client, params, last_run)

    assert incidents == expected_incidents
    assert updated_last_run == expected_last_run


@freeze_time("2025-01-01T01:10:00Z")
def test_fetch_events_success(mocker: MockerFixture):
    from ExabeamSecOpsPlatform import fetch_events

    mock_events = [
        {"caseId": "B", "_time": "2025-01-01T00:00:00Z"},
        {"caseId": "C", "_time": "2025-01-01T01:01:00Z"},
        {"caseId": "D", "_time": "2025-01-01T01:02:00Z"},
    ]
    mock_new_start_time = "2025-01-01T01:02:00.000Z"
    mock_new_last_fetched_ids = ["D"]
    mock_get_cases_in_batches = mocker.patch(
        "ExabeamSecOpsPlatform.get_cases_in_batches",
        return_value=(mock_events, mock_new_start_time, mock_new_last_fetched_ids),
    )

    mock_client = MockClient("example.com", "", "", False, False)
    max_fetch = 1000
    prev_start_time = "2025-01-01T00:00:00Z"
    prev_last_fetched_ids = ["A"]
    last_run = {"time": prev_start_time, "last_ids": prev_last_fetched_ids}

    events, next_run = fetch_events(mock_client, max_fetch, last_run)

    assert events == mock_events
    assert next_run == {"time": mock_new_start_time, "last_ids": mock_new_last_fetched_ids}

    assert mock_get_cases_in_batches.call_count == 1
    assert mock_get_cases_in_batches.call_args.kwargs == {
        "client": mock_client,
        "start_time": prev_start_time,
        "end_time": "2025-01-01T01:10:00Z",  # Same as frozen time
        "last_fetched_ids": prev_last_fetched_ids,
        "max_fetch": max_fetch,
    }


@pytest.mark.parametrize(
    "test_data_file_name",
    [
        pytest.param("fetch-events-no-cases.json", id="Empty batch"),
        pytest.param("fetch-events-partial-batch.json", id="Partial batch"),
        pytest.param("fetch-events-max-fetch.json", id="Max fetch less than batch size"),
        pytest.param("fetch-events-duplicate-cases.json", id="Batch with duplicates"),
    ],
)
def test_get_cases_in_batches(mocker: MockerFixture, test_data_file_name: str):
    """
    GIVEN:
        - Responses from Exabeam API for case search.
    WHEN:
        - The `get_cases_in_batches` function is called.
    THEN:
        - Ensure the function returns the expected events and the correct start time and last fetched IDs.
    """
    from ExabeamSecOpsPlatform import get_cases_in_batches

    test_data = util_load_json(f"test_data/{test_data_file_name}")

    mock_client = MockClient("example.com", "", "", False, False)
    mocker.patch.object(MockClient, "case_search_request", side_effect=test_data["mock_responses"])

    events, start_time, last_fetched_ids = get_cases_in_batches(
        client=mock_client,
        start_time="2025-04-04T01:07:33Z",
        end_time="2025-09-01T00:00:00Z",
        last_fetched_ids=test_data["prev_last_fetched_ids"],
        max_fetch=test_data["max_fetch"],
    )

    assert events == test_data["expected_events"]
    assert start_time == test_data["expected_new_start_time"]
    assert sorted(last_fetched_ids) == sorted(test_data["expected_new_last_fetched_ids"])


@freeze_time("2025-01-01T01:10:00Z")
def test_get_events_command(mocker: MockerFixture):
    """
    GIVEN:
        - A mock client and arguments for the get_events function.
    WHEN:
        - The `get_events_command` function is called.
    THEN:
        - Ensure get_cases_in_batches is called with the correct arguments.
        - Ensure tableToMarkdown is called with the correct arguments.
        - Ensure the function returns the expected events and CommandResults.
    """
    from ExabeamSecOpsPlatform import get_events_command

    mock_client = MockClient("example.com", "", "", False, False)
    mock_events_data = [{"caseId": "123", "name": "Test Event", "_time": "2025-01-02T00:00:00Z"}]

    mock_get_cases_in_batches = mocker.patch(
        "ExabeamSecOpsPlatform.get_cases_in_batches",
        return_value=(mock_events_data, "2025-01-02T00:00:00Z", ["123"]),
    )
    mock_table_to_markdown = mocker.patch("ExabeamSecOpsPlatform.tableToMarkdown")

    args = {
        "start_time": "1 day ago",
        "end_time": "now",
        "limit": "100",
    }

    events, results = get_events_command(mock_client, args)

    assert events == mock_events_data
    assert isinstance(results, CommandResults)

    assert mock_get_cases_in_batches.call_count == 1
    assert mock_get_cases_in_batches.call_args_list[0][1] == {
        "client": mock_client,
        "start_time": "2024-12-31T01:10:00Z",  # 1 day ago compared to frozen time
        "end_time": "2025-01-01T01:10:00Z",  # current frozen time
        "last_fetched_ids": [],
        "max_fetch": 100,
    }

    assert mock_table_to_markdown.call_args_list[0][0] == ("Events", mock_events_data)