IBMSecurityGuardium

Collect events from IBM Guardium Data Security Center.

Analytics & SIEM · IBM Guardium

Details

IDIBMSecurityGuardium
ProviderIBM
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

IBM Security Guardium is a comprehensive data security platform that provides visibility and protection for sensitive data across databases, data warehouses, big data platforms, and cloud environments.

This integration enables the collection of security events from IBM Guardium Data Security Center.

Configure IBM Security Guardium in Cortex

Parameter Description Required
Server URL The URL of your IBM Guardium instance. The default value is an example - replace it with your specific instance URL. True
API Key The API Key for authentication True
API Secret The API Secret for authentication True
Report ID The ID of the report to fetch events from. True
Fetch events Whether to automatically fetch events. False
Maximum number of events to fetch Maximum number of events to fetch per run. Default is 10000. Recommended maximum is 10000. False
Timestamp Field Name The display name of the header in the report that contains the timestamp field, e.g., “Date created (local time)”.
Note: This field name varies between different reports.
False (Required when Fetch events is enabled)
Trust any certificate (not secure) Trust any certificate (not secure). False
Use system proxy settings Use system proxy settings. False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

ibm-guardium-get-events


Manual command to fetch and display events.

Base Command

ibm-guardium-get-events

Input

Argument Name Description Required
should_push_events If true, the command creates events; otherwise, it only displays them. Possible values are: true, false. Default is false. Required
timestamp_field The name of the field in the event data that contains the timestamp.
Note: This field name varies between different reports. If not provided, uses the value from integration configuration.
Optional (Required when should_push_events is true)
limit Maximum number of results to return. Maximum allowed is 1000. Default is 50. Optional
start_time Start time for fetching events. Supports ISO format (“2023-01-01T00:00:00”) or natural language (“7 days ago”, “yesterday”, “1 week ago”). Defaults to 1 hour ago if not provided. Optional
end_time End time for fetching events. Supports ISO format (“2023-01-01T23:59:59”) or natural language (“2 hours ago”, “now”). If not provided, defaults to now. Optional

Context Output

There is no context output for this command.

Command Example

!ibm-guardium-get-events limit=50 start_time="2024-01-01T00:00:00" end_time="2024-01-01T23:59:59" should_push_events=true

Human Readable Output

IBM Guardium Events

Client IP Database User Source Program Server IP Service Name Database Name Session Start Time
10.0.0.1 admin SQLClient 10.0.0.100 PROD_DB customers 2024-01-01 10:30:00
10.0.0.2 user1 AppServer 10.0.0.100 PROD_DB orders 2024-01-01 10:31:15

Additional Information

Note: The integration fetches events from the last 12 hours by default on first run. This accounts for IBM Guardium’s event indexing delays and ensures events are captured even when indexed with significant delays.

Configuration parameters

  • url — Server URL (required)
  • credentials — API Key (required)
  • report_id — Report ID (required)
  • isFetchEvents — Fetch events
  • max_fetch — Maximum number of events to fetch
  • timestamp_field — Timestamp Field Name
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • ibm-guardium-get-events

    Manual command to fetch and display events.

import pytest
import json
from IBMSecurityGuardium import (
    Client,
    extract_field_mapping,
    get_event_hash,
    map_event,
    validate_timestamp_field,
    deduplicate_events,
    build_ignore_list,
    fetch_events_command,
    get_events_command,
)
from CommonServerPython import DemistoException


def util_load_json(path):
    """Load JSON file from test_data directory."""
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def util_create_batch_response(
    num_events, batch_num=1, timestamp_base="2025-01-01 10:00:00.0", limit_reached=True, final_result=False
):
    """
    Create a batch response with specified number of events.

    Args:
        num_events: Number of events to include in the batch
        batch_num: Batch number for unique event IDs
        timestamp_base: Base timestamp string
        limit_reached: Whether the limit was reached (indicates more data available)
        final_result: Whether this is the final result

    Returns:
        Dictionary representing API response
    """
    base_response = util_load_json("test_data/sample_api_response.json")

    # Generate events
    events = []
    for i in range(num_events):
        # Parse base timestamp and add seconds
        from datetime import datetime, timedelta

        base_dt = datetime.strptime(timestamp_base, "%Y-%m-%d %H:%M:%S.%f")
        event_dt = base_dt + timedelta(seconds=i + (batch_num - 1) * 1000)
        timestamp = event_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]  # Keep one decimal place

        events.append(
            {
                "results": {
                    "1": "8.8.8.8",
                    "2": "test",
                    "3": "sqlcmd",
                    "4": "10.130.17.21",
                    "5": "MS SQL SERVER",
                    "6": "MASTER",
                    "7": timestamp,
                }
            }
        )

    base_response["result"]["data"] = events
    base_response["result"]["limit_reached"] = limit_reached
    base_response["result"]["final_result"] = final_result
    return base_response


BASE_URL = "https://guardium.security.ibm.com"
REPORT_ID = "test_report_id"


@pytest.fixture
def client():
    """
    Given:
        - Base URL and authentication credentials
    When:
        - Creating a Client instance
    Then:
        - Ensure the client is properly initialized
    """
    return Client(base_url=BASE_URL, auth=("key", "secret"), verify=True, proxy=False)


class TestExtractFieldMapping:
    """Tests for extract_field_mapping function"""

    def test_extract_field_mapping_with_nls_value(self):
        """
        Given:
            - API response with report headers containing nls_value
        When:
            - Calling extract_field_mapping
        Then:
            - Ensure field mapping uses nls_value (user-friendly names)
        """
        response = util_load_json("test_data/sample_api_response.json")
        result = extract_field_mapping(response)
        assert result == {
            "1": "Client IP",
            "2": "Database User",
            "3": "Source Program",
            "4": "Server IP",
            "5": "Service Name",
            "6": "Database Name",
            "7": "Session Start Time",
        }

    def test_extract_field_mapping_missing_data(self):
        """
        Given:
            - API response missing required fields
        When:
            - Calling extract_field_mapping
        Then:
            - Ensure DemistoException is raised
        """
        with pytest.raises(DemistoException, match="Failed to extract field mapping"):
            extract_field_mapping({"result": {}})


class TestGetEventHash:
    """Tests for get_event_hash function"""

    def test_get_event_hash_consistent(self):
        """
        Given:
            - Same event dictionary
        When:
            - Calling get_event_hash multiple times
        Then:
            - Ensure the hash is consistent
        """
        event = {"field1": "value1", "field2": "value2"}
        hash1 = get_event_hash(event)
        hash2 = get_event_hash(event)
        assert hash1 == hash2
        assert len(hash1) == 16

    def test_get_event_hash_different_events(self):
        """
        Given:
            - Two different events
        When:
            - Calling get_event_hash on each
        Then:
            - Ensure the hashes are different
        """
        event1 = {"field1": "value1"}
        event2 = {"field1": "value2"}
        hash1 = get_event_hash(event1)
        hash2 = get_event_hash(event2)
        assert hash1 != hash2

    def test_get_event_hash_order_independent(self):
        """
        Given:
            - Same event with fields in different order
        When:
            - Calling get_event_hash
        Then:
            - Ensure the hash is the same (order-independent)
        """
        event1 = {"field1": "value1", "field2": "value2"}
        event2 = {"field2": "value2", "field1": "value1"}
        hash1 = get_event_hash(event1)
        hash2 = get_event_hash(event2)
        assert hash1 == hash2


class TestMapEvent:
    """Tests for map_event function"""

    def test_map_event_with_mapping(self):
        """
        Given:
            - Raw event with numbered fields and field mapping
        When:
            - Calling map_event
        Then:
            - Ensure event fields are mapped to readable names
        """
        field_mapping = {"1": "ClientIP", "2": "UserName", "3": "Action"}
        raw_event = {"1": "10.0.0.1", "2": "test", "3": "login"}
        result = map_event(raw_event, field_mapping)
        assert result == {"ClientIP": "10.0.0.1", "UserName": "test", "Action": "login"}

    def test_map_event_unmapped_fields(self):
        """
        Given:
            - Raw event with fields not in mapping
        When:
            - Calling map_event
        Then:
            - Ensure unmapped fields keep their original keys
        """
        field_mapping = {"1": "ClientIP"}
        raw_event = {"1": "10.0.0.1", "2": "test"}
        result = map_event(raw_event, field_mapping)
        assert result == {"ClientIP": "10.0.0.1", "2": "test"}


class TestValidateTimestampField:
    """Tests for validate_timestamp_field function"""

    @pytest.mark.parametrize(
        "timestamp_field,field_mapping,is_fetch_flow",
        [
            ("Session Start Time", {"1": "Session Start Time", "2": "Client IP"}, True),
            ("Created Date", {"1": "Created Date", "2": "Modified Date"}, False),
            ("Database Name", {"1": "Client IP", "2": "Database Name", "3": "User"}, True),
        ],
    )
    def test_validate_timestamp_field_success(self, timestamp_field, field_mapping, is_fetch_flow):
        """
        Given:
            - Valid timestamp field names that exist in field mapping
        When:
            - Calling validate_timestamp_field with different flow contexts
        Then:
            - Ensure no exception is raised
        """
        # Should not raise any exception
        validate_timestamp_field(timestamp_field, field_mapping, is_fetch_flow)

    @pytest.mark.parametrize(
        "timestamp_field,field_mapping,is_fetch_flow,expected_error",
        [
            ("", {"1": "Field1"}, True, "Timestamp Field Name is required when using fetch events"),
            (None, {"1": "Field1"}, True, "Timestamp Field Name is required when using fetch events"),
            ("", {"1": "Field1"}, False, "Timestamp Field Name is required when should_push_events=true"),
            (None, {"1": "Field1"}, False, "Timestamp Field Name is required when should_push_events=true"),
            (
                "NonExistent",
                {"1": "Field1", "2": "Field2"},
                True,
                "Timestamp field 'NonExistent' not found in this report's headers",
            ),
            (
                "Invalid Field",
                {"1": "Client IP", "2": "Session Start Time"},
                False,
                "Timestamp field 'Invalid Field' not found in this report's headers",
            ),
        ],
    )
    def test_validate_timestamp_field_failures(self, timestamp_field, field_mapping, is_fetch_flow, expected_error):
        """
        Given:
            - Invalid timestamp fields (empty, None, or non-existent)
            - Different flow contexts (fetch-events vs get-events with should_push_events)
        When:
            - Calling validate_timestamp_field
        Then:
            - Ensure DemistoException is raised with context-appropriate error message
        """
        with pytest.raises(DemistoException, match=expected_error):
            validate_timestamp_field(timestamp_field, field_mapping, is_fetch_flow)


class TestDeduplicateEvents:
    """Tests for deduplicate_events function"""

    def test_deduplicate_events_empty_list(self):
        """
        Given:
            - Empty events list
        When:
            - Calling deduplicate_events
        Then:
            - Ensure empty list is returned
        """
        result = deduplicate_events([], {}, "timestamp")
        assert result == []

    def test_deduplicate_events_no_duplicates(self):
        """
        Given:
            - Events with different timestamps
        When:
            - Calling deduplicate_events
        Then:
            - Ensure all events are returned
        """
        events = [
            {"timestamp": "2025-01-01 10:00:00", "id": "1"},
            {"timestamp": "2025-01-01 11:00:00", "id": "2"},
            {"timestamp": "2025-01-01 12:00:00", "id": "3"},
        ]
        last_run = {"last_fetch_time": "2025-01-01 09:00:00", "fetched_event_hashes": []}
        result = deduplicate_events(events, last_run, "timestamp")
        assert len(result) == 3

    def test_deduplicate_events_with_duplicates(self):
        """
        Given:
            - Events in descending order with same timestamp as last_fetch_time and matching hashes
        When:
            - Calling deduplicate_events
        Then:
            - Ensure duplicate events are filtered out
        """
        event1 = {"timestamp": "2025-01-01 10:00:00", "id": "2"}
        event2 = {"timestamp": "2025-01-01 10:00:00", "id": "1"}
        event3 = {"timestamp": "2025-01-01 09:00:00", "id": "0"}
        events = [event1, event2, event3]  # Descending order (newest first)

        hash2 = get_event_hash(event2)
        last_run = {"last_fetch_time": "2025-01-01 10:00:00", "fetched_event_hashes": [hash2]}

        result = deduplicate_events(events, last_run, "timestamp")
        # event2 is filtered (duplicate), event1 and event3 are kept
        assert len(result) == 2
        # Check that event2 was filtered and event1 and event3 are in the results
        event_ids = [e["id"] for e in result]
        assert "1" not in event_ids
        assert "2" in event_ids
        assert "0" in event_ids

    def test_deduplicate_events_optimization(self):
        """
        Given:
            - Events in descending order where all timestamps are greater than last_fetch_time
            - Both event hashes are in the ignore list
        When:
            - Calling deduplicate_events
        Then:
            - Ensure all events are added without duplicate checking (because timestamp > last_fetch_time)
        """
        event1 = {"timestamp": "2025-01-01 12:00:00", "id": "2"}
        event2 = {"timestamp": "2025-01-01 11:00:00", "id": "1"}
        events = [event1, event2]  # Descending order (newest first)

        # Get hashes of both events and add them to the ignore list (Hypothetical scenario)
        hash1 = get_event_hash(event1)
        hash2 = get_event_hash(event2)
        last_run = {"last_fetch_time": "2025-01-01 10:00:00", "fetched_event_hashes": [hash1, hash2]}

        result = deduplicate_events(events, last_run, "timestamp")
        # Both events should be kept because their timestamps are greater than last_fetch_time
        assert len(result) == 2


class TestBuildIgnoreList:
    """Tests for build_ignore_list function"""

    def test_build_ignore_list_empty_events(self):
        """
        Given:
            - Empty events list
        When:
            - Calling build_ignore_list
        Then:
            - Ensure empty set is returned
        """
        result = build_ignore_list([], "timestamp")
        assert result == set()

    def test_build_ignore_list_single_timestamp(self):
        """
        Given:
            - Multiple events with same timestamp
        When:
            - Calling build_ignore_list
        Then:
            - Ensure all event hashes are in ignore list
        """
        events = [
            {"timestamp": "2025-01-01 10:00:00", "id": "1"},
            {"timestamp": "2025-01-01 10:00:00", "id": "2"},
            {"timestamp": "2025-01-01 10:00:00", "id": "3"},
        ]
        result = build_ignore_list(events, "timestamp")
        assert len(result) == 3

    def test_build_ignore_list_multiple_timestamps(self):
        """
        Given:
            - Events in descending order with different timestamps
        When:
            - Calling build_ignore_list
        Then:
            - Ensure only events with most recent (first) timestamp are in ignore list
        """
        events = [
            {"timestamp": "2025-01-01 12:00:00", "id": "3"},
            {"timestamp": "2025-01-01 12:00:00", "id": "4"},
            {"timestamp": "2025-01-01 11:00:00", "id": "2"},
            {"timestamp": "2025-01-01 10:00:00", "id": "1"},
        ]  # Descending order (newest first)
        result = build_ignore_list(events, "timestamp")
        assert len(result) == 2

    def test_build_ignore_list_missing_timestamp_field(self):
        """
        Given:
            - Events missing the timestamp field
        When:
            - Calling build_ignore_list
        Then:
            - Ensure DemistoException is raised
        """
        events = [{"id": "1"}]
        with pytest.raises(DemistoException, match="Timestamp field 'timestamp' not found"):
            build_ignore_list(events, "timestamp")


class TestTestModule:
    """Tests for test_module function"""

    def test_test_module_success(self, client, requests_mock):
        """
        Given:
            - Valid client and report ID
        When:
            - Calling test_module_command
        Then:
            - Ensure 'ok' is returned when API call succeeds
        """
        from IBMSecurityGuardium import test_module_command

        response = util_load_json("test_data/no_data_response.json")

        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)
        result = test_module_command(client, REPORT_ID, is_fetch=False, timestamp_field=None)
        assert result == "ok"

    def test_test_module_auth_error(self, client, requests_mock):
        """
        Given:
            - Invalid credentials
        When:
            - Calling test_module_command
        Then:
            - Ensure authorization error message is returned
        """
        from IBMSecurityGuardium import test_module_command

        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", status_code=403, text="Forbidden")
        result = test_module_command(client, REPORT_ID, is_fetch=False, timestamp_field=None)
        assert "Authorization Error" in result


class TestFetchEvents:
    """Tests for fetch_events function"""

    def test_fetch_events_first_run(self, client, requests_mock):
        """
        Given:
            - Empty last_run (first fetch)
        When:
            - Calling fetch_events_command
        Then:
            - Ensure events are fetched and next_run is set correctly
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=10, last_run={}, timestamp_field="Session Start Time"
        )

        assert len(events) == 1
        assert "Session Start Time" in events[0]
        assert next_run["last_fetch_time"] == "2025-06-07 16:52:57.0"
        assert len(next_run["fetched_event_hashes"]) == 1

    def test_fetch_events_no_events(self, client, requests_mock):
        """
        Given:
            - API response with no events
        When:
            - Calling fetch_events_command
        Then:
            - Ensure empty events list and unchanged last_run
        """
        response = util_load_json("test_data/no_data_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        last_run = {"last_fetch_time": "2025-06-07T15:00:00.000Z"}
        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=10, last_run=last_run, timestamp_field="Date created (local time)"
        )

        assert len(events) == 0
        assert next_run == last_run

    def test_fetch_events_pagination_multiple_batches(self, client, requests_mock, monkeypatch):
        """
        Given:
            - max_fetch of 25 events (requires 3 batches with MAX_BATCH_SIZE=10)
        When:
            - Calling fetch_events_command
        Then:
            - Ensure offset is updated correctly for each batch
            - Ensure all batches are fetched until max_fetch is reached
            - Ensure offset increments by the number of events returned in each batch
        """
        import IBMSecurityGuardium

        monkeypatch.setattr(IBMSecurityGuardium, "MAX_BATCH_SIZE", 10)

        call_count = 0

        def mock_response(request, context):
            nonlocal call_count
            call_count += 1
            payload = request.json()
            offset = payload["offset"]
            fetch_size = payload["fetch_size"]

            # Batch 1: offset=0, fetch_size=10, return 10 events
            if offset == 0:
                assert fetch_size == 10
                return json.dumps(util_create_batch_response(10, batch_num=1, limit_reached=True))
            # Batch 2: offset=10, fetch_size=10, return 10 events
            elif offset == 10:
                assert fetch_size == 10
                return json.dumps(util_create_batch_response(10, batch_num=2, limit_reached=True))
            # Batch 3: offset=20, fetch_size=5 (remaining), return 5 events
            elif offset == 20:
                assert fetch_size == 5  # Only 5 remaining to reach max_fetch=25
                return json.dumps(util_create_batch_response(5, batch_num=3, limit_reached=False, final_result=True))
            else:
                context.status_code = 400
                return json.dumps({"error": f"Unexpected offset: {offset}"})

        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=mock_response)

        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=25, last_run={}, timestamp_field="Session Start Time"
        )

        # Verify all 25 events were fetched
        assert len(events) == 25
        # Verify 3 API calls were made
        assert call_count == 3

    def test_fetch_events_stops_when_less_than_batch_size_returned(self, client, requests_mock, monkeypatch):
        """
        Given:
            - API returns fewer events than requested batch_size
        When:
            - Calling fetch_events_command
        Then:
            - Ensure the loop breaks (no more API calls)
            - Ensure offset is not incremented after the break
        """
        import IBMSecurityGuardium

        monkeypatch.setattr(IBMSecurityGuardium, "MAX_BATCH_SIZE", 10)

        call_count = 0

        def mock_response(request, context):
            nonlocal call_count
            call_count += 1
            payload = request.json()
            offset = payload["offset"]

            # First batch: return 5 events (less than batch_size of 10)
            if offset == 0:
                return json.dumps(util_create_batch_response(5, batch_num=1, limit_reached=False))
            else:
                # Should not reach here
                context.status_code = 400
                return json.dumps({"error": "Should not make second API call"})

        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=mock_response)

        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=100, last_run={}, timestamp_field="Session Start Time"
        )

        # Verify only 5 events were fetched
        assert len(events) == 5
        # Verify only 1 API call was made (loop broke after first batch)
        assert call_count == 1

    def test_fetch_events_stops_when_no_events_returned(self, client, requests_mock, monkeypatch):
        """
        Given:
            - API returns empty data array
        When:
            - Calling fetch_events_command
        Then:
            - Ensure the loop breaks immediately
            - Ensure no events are collected
        """
        import IBMSecurityGuardium

        monkeypatch.setattr(IBMSecurityGuardium, "MAX_BATCH_SIZE", 10)

        response = util_load_json("test_data/no_data_response.json")
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=json.dumps(response))

        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=100, last_run={}, timestamp_field="Date created (local time)"
        )

        assert len(events) == 0

    def test_fetch_events_stops_at_max_fetch_mid_batch(self, client, requests_mock, monkeypatch):
        """
        Given:
            - max_fetch=15 and batch returns 10 events in first call
            - Second batch would exceed max_fetch
        When:
            - Calling fetch_events_command
        Then:
            - Ensure only 5 events are requested in second batch
            - Ensure total events equals max_fetch exactly
            - Ensure loop stops after reaching max_fetch
        """
        import IBMSecurityGuardium

        monkeypatch.setattr(IBMSecurityGuardium, "MAX_BATCH_SIZE", 10)

        call_count = 0

        def mock_response(request, context):
            nonlocal call_count
            call_count += 1
            payload = request.json()
            offset = payload["offset"]
            fetch_size = payload["fetch_size"]

            if offset == 0:
                assert fetch_size == 10
                return json.dumps(util_create_batch_response(10, batch_num=1, limit_reached=True))
            elif offset == 10:
                # Should request only 5 (remaining to reach 15)
                assert fetch_size == 5
                return json.dumps(util_create_batch_response(5, batch_num=2, limit_reached=False, final_result=True))
            else:
                context.status_code = 400
                return json.dumps({"error": "Should not make third API call"})

        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=mock_response)

        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=15, last_run={}, timestamp_field="Session Start Time"
        )

        assert len(events) == 15
        assert call_count == 2

    def test_fetch_events_field_mapping_extracted_once(self, client, requests_mock, monkeypatch):
        """
        Given:
            - Multiple batches of events
        When:
            - Calling fetch_events_command
        Then:
            - Ensure field mapping is extracted only from the first batch (offset == 0)
            - Ensure timestamp field is determined only from the first batch
        """
        import IBMSecurityGuardium

        monkeypatch.setattr(IBMSecurityGuardium, "MAX_BATCH_SIZE", 10)

        call_count = 0

        def mock_response(request, context):
            nonlocal call_count
            call_count += 1
            payload = request.json()
            offset = payload["offset"]

            # Both batches use the same structure from sample_api_response
            # First batch: full batch_size
            if offset == 0:
                return json.dumps(util_create_batch_response(10, batch_num=1, limit_reached=True))
            # Second batch: partial (triggers stop)
            else:
                return json.dumps(util_create_batch_response(3, batch_num=2, limit_reached=False))

        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=mock_response)

        events, next_run = fetch_events_command(
            client, REPORT_ID, max_fetch=50, last_run={}, timestamp_field="Session Start Time"
        )

        # Verify events are properly mapped
        assert len(events) == 13  # 10 + 3
        assert "Session Start Time" in events[0]
        assert "Client IP" in events[0]


class TestGetEventsCommand:
    """Tests for get_events_command function"""

    def test_get_events_command_no_events(self, client, requests_mock):
        """
        Given:
            - API response with no events
        When:
            - Calling get_events_command
        Then:
            - Ensure empty events list is returned
        """
        response = util_load_json("test_data/no_data_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        events, results, timestamp_field_result = get_events_command(
            client, REPORT_ID, {}, timestamp_field="Date created (local time)"
        )

        assert len(events) == 0
        assert timestamp_field_result is None

    def test_get_events_command_invalid_time_format(self, client):
        """
        Given:
            - Invalid time format in arguments
        When:
            - Calling get_events_command
        Then:
            - Ensure DemistoException is raised
        """
        args = {"start_time": "invalid-date"}
        with pytest.raises(DemistoException, match="Invalid start_time format"):
            get_events_command(client, REPORT_ID, args, timestamp_field="Session Start Time")

    def test_get_events_command_with_should_push_events_valid_timestamp(self, client, requests_mock):
        """
        Given:
            - should_push_events is True and valid timestamp field is provided
        When:
            - Calling get_events_command
        Then:
            - Ensure timestamp_field_result is returned for XSIAM push
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        args = {"should_push_events": "true", "timestamp_field": "Session Start Time"}
        events, results, timestamp_field_result = get_events_command(client, REPORT_ID, args, timestamp_field="")

        assert len(events) == 1
        assert timestamp_field_result == "Session Start Time"

    def test_get_events_command_with_should_push_events_uses_config_timestamp(self, client, requests_mock):
        """
        Given:
            - should_push_events is True and no timestamp_field in args (uses config)
        When:
            - Calling get_events_command
        Then:
            - Ensure timestamp_field from config is used and returned
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        args = {"should_push_events": "true"}
        events, results, timestamp_field_result = get_events_command(
            client, REPORT_ID, args, timestamp_field="Session Start Time"
        )

        assert len(events) == 1
        assert timestamp_field_result == "Session Start Time"

    def test_get_events_command_with_should_push_events_invalid_timestamp(self, client, requests_mock):
        """
        Given:
            - should_push_events is True and invalid timestamp field is provided
        When:
            - Calling get_events_command
        Then:
            - Ensure DemistoException is raised about invalid timestamp field
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        args = {"should_push_events": "true", "timestamp_field": "Invalid Field"}
        with pytest.raises(DemistoException, match="Timestamp field 'Invalid Field' not found in this report's headers"):
            get_events_command(client, REPORT_ID, args, timestamp_field="")

    def test_get_events_command_with_should_push_events_missing_timestamp(self, client, requests_mock):
        """
        Given:
            - should_push_events is True but no timestamp field is provided (neither in args nor config)
        When:
            - Calling get_events_command
        Then:
            - Ensure DemistoException is raised about missing timestamp field
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        args = {"should_push_events": "true"}
        with pytest.raises(DemistoException, match="Timestamp Field Name is required when should_push_events=true"):
            get_events_command(client, REPORT_ID, args, timestamp_field="")

    def test_get_events_command_without_should_push_events(self, client, requests_mock):
        """
        Given:
            - should_push_events is False or not provided
        When:
            - Calling get_events_command
        Then:
            - Ensure timestamp_field_result is None (no XSIAM push)
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        args = {"should_push_events": "false"}
        events, results, timestamp_field_result = get_events_command(
            client, REPORT_ID, args, timestamp_field="Session Start Time"
        )

        assert len(events) == 1
        assert timestamp_field_result is None

    def test_get_events_command_limit_exceeds_maximum(self, client):
        """
        Given:
            - limit parameter exceeds 1000
        When:
            - Calling get_events_command
        Then:
            - Ensure DemistoException is raised with appropriate error message
        """
        args = {"limit": "1500"}
        with pytest.raises(DemistoException, match="The limit parameter cannot exceed 1000"):
            get_events_command(client, REPORT_ID, args, timestamp_field="Session Start Time")

    def test_get_events_command_limit_at_maximum(self, client, requests_mock):
        """
        Given:
            - limit parameter is exactly 1000 (maximum allowed)
        When:
            - Calling get_events_command
        Then:
            - Ensure the command executes successfully without raising an error
        """
        response = util_load_json("test_data/sample_api_response.json")
        response_text = json.dumps(response)
        requests_mock.post(f"{BASE_URL}/api/v3/reports/run", text=response_text)

        args = {"limit": "1000"}
        events, results, timestamp_field_result = get_events_command(
            client, REPORT_ID, args, timestamp_field="Session Start Time"
        )

        assert len(events) == 1
        assert timestamp_field_result is None