DruvaEventCollector
Druva Ransomware Response Integration provides ransomware protection for endpoints, SaaS applications and data center workloads for Druva Ransomware Recovery customers.
Analytics & SIEM · Druva
Details
| ID | DruvaEventCollector |
|---|---|
| Provider | Druva |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
This is the Druva event collector integration for Cortex XSIAM.
Configure Druva Event Collector in Cortex
| Parameter | Required |
|---|---|
| Server URL | True |
| Client ID | True |
| Secret Key | True |
| Trust any certificate (not secure) | |
| Use system proxy settings | |
| The maximum number of events per fetch per type | |
| Events to fetch |
Additional Information
- The maximum number of events per fetch: The default value is 10,000. The API only returns up to 500 events at a time without limiting capabilities, so it is best to enter this parameter in multiples of 500.
- Resetting event fetching: Note that resetting the event fetching (clearing the integration context) will cause duplicate events to be fetched, as the integration will restart the fetch process from the beginning.
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.
druva-get-events
Gets events from Druva API in one batch (max 500). If tracker is given, only its successive events will be fetched.
Base Command
druva-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | Set this argument to true in order to create Cortex XSIAM events, otherwise the command will only display them. Possible values are: true, false. Default is false. | Required |
| tracker | A string received in a previous run, marking the point in time from which we want to fetch. For InSync events, this is a tracker. For Cybersecurity events, this is a pageToken. | Optional |
| event_types | The types of events to fetch. Possible values are: InSync events, Cybersecurity events. Default is InSync events. | Optional |
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (required)credentials— Client ID (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsmax_fetch— The maximum number of events per fetch per type.event_types— Events to fetch
Commands (1)
-
druva-get-eventsGets events from Druva API in one batch (max 500). If tracker is given, only its successive events will be fetched.
from datetime import datetime import demistomock as demisto import pytest import requests from CommonServerPython import DemistoException from DruvaEventCollector import ( DATE_FORMAT_FOR_TOKEN, MAX_FETCH, Client, _filter_old_events, fetch_events, get_events, ) from DruvaEventCollector import ( test_module as run_test_module, ) from freezegun import freeze_time RESPONSE_WITH_EVENTS_1 = { "events": [ { "eventID": 0, "eventType": "Backup", "profileName": "Default", "inSyncUserName": "user name", "clientVersion": "7.5.0r(23c42be5)", "clientOS": "Office 365 Exchange Online", "ip": "", "inSyncUserEmail": "test@test.com", "eventDetails": "", "timestamp": "2024-05-25T18:52:48Z", "inSyncUserID": 0, "profileID": 0, "initiator": None, "inSyncDataSourceID": 0, "eventState": "Backed up with Errors", "inSyncDataSourceName": "Exchange Online", "severity": 4, "facility": 23, } ], "nextPageExists": False, "tracker": "xxxx", } RESPONSE_CYBERSECURITY_EVENTS = { "events": [ { "id": 1178203423, "productID": 8193, "globalID": "e65b5695-4ef4-453b-90c6-3a31b0e6b159", "category": "EVENT", "feature": "Alerts And Notifications", "type": "Alert", "timeStamp": "2024-05-25T18:52:48Z", "details": {"eventID": 36832945, "ip": None, "clientOS": None}, } ], "nextPageToken": "cybertoken123", } RESPONSE_WITH_EVENTS_2 = { "events": [ { "eventID": 1, "eventType": "Backup", "profileName": "Default", "inSyncUserName": "user name", "clientVersion": "7.5.0r(23c42be5)", "clientOS": "Office 365 Exchange Online", "ip": "", "inSyncUserEmail": "test@test.com", "eventDetails": "", "timestamp": "2024-05-25T18:52:48Z", "inSyncUserID": 1, "profileID": 1, "initiator": None, "inSyncDataSourceID": 1, "eventState": "Backed up with Errors", "inSyncDataSourceName": "Exchange Online", "severity": 4, "facility": 23, } ], "nextPageExists": False, "tracker": "yyyy", } RESPONSE_WITHOUT_EVENTS = {"events": [], "nextPageExists": False, "tracker": "xxxx"} INVALID_RESPONSE = { "events": [ { "eventID": 0, "eventType": "Backup", "profileName": "Default", "inSyncUserName": "user name", "clientVersion": "7.5.0r(23c42be5)", "clientOS": "Office 365 Exchange Online", "ip": "", "inSyncUserEmail": "test@test.com", "eventDetails": "", "timestamp": "2024-05-25T18:52:48Z", "inSyncUserID": 0, "profileID": 0, "initiator": None, "inSyncDataSourceID": 0, "eventState": "Backed up with Errors", "inSyncDataSourceName": "Exchange Online", "severity": 4, "facility": 23, } ], "nextPageExists": False, } @pytest.fixture() def mock_client(mocker) -> Client: mocker.patch.object(Client, "login", return_value="DUMMY_TOKEN") client = Client( base_url="test", client_id="client_id", secret_key="secret_key", max_fetch=MAX_FETCH, verify=False, proxy=False, ) client._set_headers("DUMMY_TOKEN") return client def test_test_module_command(mocker, mock_client): """ Given: - test module command - an empty (yet valid) response from Druva When: - Pressing test button Then: - Test module passed """ mocker.patch.object( mock_client, "search_events", return_value={"events": [], "tracker": "DUMMY_TRACKER"}, ) result = run_test_module(client=mock_client, event_types=["InSync events"]) assert result == "ok" def test_test_module_command_failure(mocker, mock_client): """ Given: - test module command When: - Pressing test button Then: - Test module failed """ mocker.patch.object( mock_client, "_http_request", side_effect=DemistoException(message="Error: invalid_grant"), ) with pytest.raises(DemistoException): run_test_module(client=mock_client, event_types=["InSync events"]) @pytest.mark.parametrize( "event_type,mock_response,expected_tracker,expected_event_count,expected_event_id_field", [ ("InSync events", RESPONSE_WITH_EVENTS_1, "xxxx", 1, "eventID"), ], ) def test_get_events_command( mocker, mock_client, event_type, mock_response, expected_tracker, expected_event_count, expected_event_id_field ): """ Given: - get_events command for InSync events When: - running get events command Then: - Ensure events and tracker are returned as expected """ mocker.patch.object(mock_client, "search_events", return_value=mock_response) events, tracker = get_events(client=mock_client, event_type=event_type) assert tracker == expected_tracker assert len(events) == expected_event_count assert expected_event_id_field in events[0] def test_search_events_cybersecurity_normalization(mocker, mock_client): """ Given: - Cybersecurity events API response with nextPageToken When: - running search_events method with event_type="Cybersecurity events" Then: - Ensure nextPageToken is normalized to tracker in the response """ mocker.patch.object(mock_client, "_http_request", return_value=RESPONSE_CYBERSECURITY_EVENTS) response = mock_client.search_events(event_type="Cybersecurity events") # Verify normalization: tracker should be added with same value as nextPageToken assert response["tracker"] == "cybertoken123" assert response["nextPageToken"] == "cybertoken123" def test_get_events_command_failure(mocker, mock_client): """ Given: - a mocked client - mocked response: invalid response structure, since it does not have the 'tracker' key as expected When: - running get events command Then: - Ensure KeyError exception was thrown due to invalid response """ mocker.patch.object(mock_client, "search_events", return_value=INVALID_RESPONSE) with pytest.raises(KeyError): get_events(client=mock_client, event_type="InSync events") def test_refresh_access_token(mocker, mock_client): """ Given: - a mock client When: - running _refresh_access_token method Then: - Ensure exception is thrown - Ensure informative message was shown """ response = requests.Response() response.status_code = 400 mocker.patch.object( mock_client, "_http_request", side_effect=DemistoException(message="invalid_grant", res=response), ) error_message = "Error in test-module: Make sure Server URL, Client ID and Secret Key are correctly entered." with pytest.raises(DemistoException, match=error_message): mock_client._refresh_access_token() @pytest.mark.parametrize( "event_type,tracker,expected_url_suffix,mock_response", [ ("InSync events", "xxxx", "/insync/eventmanagement/v2/events?tracker=xxxx", RESPONSE_WITHOUT_EVENTS), ("InSync events", None, "/insync/eventmanagement/v2/events", RESPONSE_WITHOUT_EVENTS), ( "Cybersecurity events", "token123", "/platform/eventmanagement/v3/events?pageToken=token123", RESPONSE_CYBERSECURITY_EVENTS, ), ("Cybersecurity events", None, "/platform/eventmanagement/v3/events?pageSize=500", RESPONSE_CYBERSECURITY_EVENTS), ], ) def test_search_events_called_with(mocker, mock_client, event_type, tracker, expected_url_suffix, mock_response): """ Given: - a mock client - different event types and tracker scenarios When: - running search_events method with various parameters Then: - Ensure correct URL is constructed for each scenario """ http_mock = mocker.patch.object(mock_client, "_http_request", return_value=mock_response) mock_client.search_events(tracker=tracker, event_type=event_type) http_mock.assert_called_with( method="GET", url_suffix=expected_url_suffix, headers={"Authorization": "Bearer DUMMY_TOKEN", "accept": "application/json"}, ) def test_search_events_failure(mocker, mock_client): """ Given: - a mock client When: - running search_events method Then: - Ensure an exception was thrown due to invalid tracker """ mocker.patch.object( mock_client, "_http_request", side_effect=DemistoException(message="Error: Invalid tracker"), ) with pytest.raises(DemistoException): mock_client.search_events(tracker="xxxx") def test_fetch_events_command(mocker, mock_client): """ Given: - fetch events command When: - Running fetch-events command Then: - Ensure number of events fetched, and the next run, match the response data """ mocker.patch("DruvaEventCollector._filter_old_events", side_effect=lambda events: events) # First fetch mocker.patch.object(mock_client, "search_events", return_value=RESPONSE_WITH_EVENTS_1) first_run = {} events, tracker_for_second_run = fetch_events( client=mock_client, last_run=first_run, max_fetch=MAX_FETCH, event_types=["InSync events"] ) assert len(events) == 1 assert tracker_for_second_run["tracker_InSync events"] == "xxxx" assert events[0]["eventID"] == 0 # Second fetch mock_search_events = mocker.patch.object(mock_client, "search_events", return_value=RESPONSE_WITH_EVENTS_2) events, tracker_for_third_run = fetch_events( client=mock_client, last_run=tracker_for_second_run, max_fetch=MAX_FETCH, event_types=["InSync events"] ) # The second fetch uses the tracker from first fetch result (xxxx) mock_search_events.assert_called_with("xxxx", "InSync events") assert len(events) == 1 assert tracker_for_third_run["tracker_InSync events"] == "yyyy" assert events[0]["eventID"] == 1 @freeze_time(datetime(2022, 2, 28, 11, 10)) @pytest.mark.parametrize( "integration_context", [ ({}), ( { "Token": "DUMMY TOKEN", "expiration_time": datetime(2022, 2, 28, 10, 50).strftime(DATE_FORMAT_FOR_TOKEN), } ), ], ) def test_login_invalid_token(mocker, integration_context): """ Given: - An IntegrationContext without a Token or one that has expired When: - Build a client (for checking login) Then: - Ensure a new token is generated """ mocker.patch.object(demisto, "getIntegrationContext", return_value=integration_context) mocker.patch.object(demisto, "setIntegrationContext") mock_refresh_access_token = mocker.patch.object(Client, "_refresh_access_token", return_value=("", 0)) Client( base_url="test", client_id="client_id", secret_key="secret_key", max_fetch=MAX_FETCH, verify=False, proxy=False, ) mock_refresh_access_token.assert_called_once_with() @freeze_time(datetime(2022, 2, 28, 11, 00)) def test_login_valid_token(mocker): """ Given: - An IntegrationContext with valid Access Token When: - Build a client (for checking login) Then: - Ensure a new token is not generated """ mocker.patch.object( demisto, "getIntegrationContext", return_value={ "Token": "DUMMY TOKEN", "expiration_time": datetime(2022, 2, 28, 11, 10).strftime(DATE_FORMAT_FOR_TOKEN), }, ) mocker.patch.object(demisto, "setIntegrationContext") mock_refresh_access_token = mocker.patch.object(Client, "_refresh_access_token", return_value=("DUMMY_TOKEN", 1800)) Client( base_url="test", client_id="client_id", secret_key="secret_key", max_fetch=MAX_FETCH, verify=False, proxy=False, ) assert not mock_refresh_access_token.called def test_max_fetch_validation(): """ Given: - invalid max_fetch (more than 10,000) When: - init the client Then: - DemistoException is thrown with an appropriate message """ with pytest.raises(DemistoException, match=f"The maximum number of events per fetch should be between 1 - {MAX_FETCH}"): Client( base_url="test", client_id="client_id", secret_key="secret_key", max_fetch=(MAX_FETCH + 1), verify=False, proxy=False, ) def test_fetch_events_invalid_tracker(mocker, mock_client): """ Given: - fetch events command When: - Running fetch-events command - Mocking the second fetch to throw an exception to Invalid tracker Then: - Ensure exception is caught - Ensure same tracker is returned (as we got at the previous call) - Ensure no events are returned """ mocker.patch("DruvaEventCollector._filter_old_events", side_effect=lambda events: events) # First fetch mocker.patch.object(mock_client, "search_events", return_value=RESPONSE_WITH_EVENTS_1) events, tracker_for_second_run = fetch_events( client=mock_client, last_run={}, max_fetch=MAX_FETCH, event_types=["InSync events"] ) # Second fetch mocker.patch.object(mock_client, "search_events", side_effect=Exception("Invalid tracker")) events, tracker_for_third_run = fetch_events( client=mock_client, last_run=tracker_for_second_run, max_fetch=MAX_FETCH, event_types=["InSync events"] ) # same tracker should be returned when "Invalid tracker" exception is thrown and no events assert tracker_for_third_run["tracker_InSync events"] == tracker_for_second_run["tracker_InSync events"] assert events == [] @pytest.mark.parametrize( "event_types,expected_tracker_keys,mock_responses", [ (["InSync events"], ["tracker_InSync events"], {"InSync events": RESPONSE_WITH_EVENTS_1}), (["Cybersecurity events"], ["tracker_Cybersecurity events"], {"Cybersecurity events": RESPONSE_CYBERSECURITY_EVENTS}), ( ["InSync events", "Cybersecurity events"], ["tracker_InSync events", "tracker_Cybersecurity events"], {"InSync events": RESPONSE_WITH_EVENTS_1, "Cybersecurity events": RESPONSE_CYBERSECURITY_EVENTS}, ), ], ) def test_fetch_events_multiple_types(mocker, mock_client, event_types, expected_tracker_keys, mock_responses): """ Given: - fetch events command with different event type configurations When: - Running fetch-events command with single or multiple event types Then: - Ensure correct trackers are stored for each event type - Ensure _time and source_log_type fields are added to events """ mocker.patch("DruvaEventCollector._filter_old_events", side_effect=lambda events: events) def mock_search(tracker=None, event_type="InSync events"): return mock_responses.get(event_type, RESPONSE_WITH_EVENTS_1) mocker.patch.object(mock_client, "search_events", side_effect=mock_search) events, next_run = fetch_events(client=mock_client, last_run={}, max_fetch=MAX_FETCH, event_types=event_types) # Verify all expected tracker keys are present for key in expected_tracker_keys: assert key in next_run # Verify event count matches number of event types assert len(events) == len(event_types) # Verify _time and source_log_type fields are added for event in events: assert "_time" in event assert "source_log_type" in event # Verify source_log_type has correct value assert event["source_log_type"] in ["insync_events", "cybersecurity_events"] @freeze_time(datetime(2024, 5, 25, 19, 30, 0)) @pytest.mark.parametrize( "events,expected_count,expected_ids", [ pytest.param( [{"timestamp": "2024-05-25T18:52:48Z", "eventID": 1}, {"timestamp": "2024-05-25T19:00:00Z", "eventID": 2}], 2, [1, 2], id="all_events_within_1h_kept", ), pytest.param( [{"timestamp": "2024-05-25T17:00:00Z", "eventID": 1}, {"timestamp": "2024-05-25T18:00:00Z", "eventID": 2}], 0, [], id="all_events_older_than_1h_dropped", ), pytest.param( [ {"timestamp": "2024-05-25T18:00:00Z", "eventID": 1}, {"timestamp": "2024-05-25T18:30:00Z", "eventID": 2}, {"timestamp": "2024-05-25T19:00:00Z", "eventID": 3}, ], 2, [2, 3], id="mixed_old_and_new_events", ), pytest.param( [], 0, [], id="empty_events_list", ), pytest.param( [ {"timeStamp": "2024-05-25T18:00:00Z", "eventID": 1}, {"timeStamp": "2024-05-25T19:00:00Z", "eventID": 2}, ], 1, [2], id="cybersecurity_camelCase_timeStamp_field", ), pytest.param( [ {"eventID": 1}, {"timestamp": "2024-05-25T19:00:00Z", "eventID": 2}, ], 2, [1, 2], id="missing_timestamp_kept_not_dropped", ), ], ) def test_filter_old_events(events, expected_count, expected_ids): """ Given: - Various event lists with different timestamp scenarios (frozen time is 19:30, cutoff is 18:30) including both 'timestamp' (InSync) and 'timeStamp' (Cybersecurity) field formats When: - Calling _filter_old_events Then: - Only events within the last hour are kept """ result = _filter_old_events(events) assert len(result) == expected_count assert [e["eventID"] for e in result] == expected_ids @freeze_time(datetime(2024, 5, 25, 19, 30, 0)) def test_filter_old_events_logs_dropped_count(mocker): """ Given: - Events older than 1 hour When: - Calling _filter_old_events Then: - A debug log is emitted with the count of dropped events """ debug_mock = mocker.patch.object(demisto, "debug") events = [{"timestamp": "2024-05-25T17:00:00Z", "eventID": 1}] _filter_old_events(events) debug_mock.assert_called_once() assert "dropped 1 events" in debug_mock.call_args[0][0] @freeze_time(datetime(2024, 5, 25, 19, 30, 0)) def test_fetch_events_no_tracker_filters_old_events(mocker, mock_client): """ Given: - First fetch (empty last_run, no tracker) - API returns events, some older than 1 hour and some newer When: - Running fetch_events with no tracker Then: - Old events are filtered out - Tracker is still saved for subsequent fetches """ response_mixed = { "events": [ { "eventID": 100, "eventType": "Backup", "timestamp": "2024-05-25T18:00:00Z", # older than 1h (cutoff 18:30) - dropped "severity": 4, "facility": 23, }, { "eventID": 101, "eventType": "Backup", "timestamp": "2024-05-25T19:00:00Z", # within 1h - kept "severity": 4, "facility": 23, }, ], "nextPageExists": False, "tracker": "new_tracker_value", } mocker.patch.object(mock_client, "search_events", return_value=response_mixed) events, next_run = fetch_events(client=mock_client, last_run={}, max_fetch=MAX_FETCH, event_types=["InSync events"]) # Only the newer event should be kept assert len(events) == 1 assert events[0]["eventID"] == 101 # Tracker should still be saved assert next_run["tracker_InSync events"] == "new_tracker_value" @freeze_time(datetime(2024, 5, 25, 19, 30, 0)) def test_fetch_events_with_tracker_does_not_filter(mocker, mock_client): """ Given: - A subsequent fetch (last_run has a tracker) - API returns events older than 1 hour When: - Running fetch_events with an existing tracker Then: - No events are filtered (filtering only applies on first fetch when no tracker) """ mocker.patch.object(mock_client, "search_events", return_value=RESPONSE_WITH_EVENTS_1) events, next_run = fetch_events( client=mock_client, last_run={"tracker_InSync events": "existing_tracker"}, max_fetch=MAX_FETCH, event_types=["InSync events"], ) # All events should be kept because tracker exists (not a first fetch) assert len(events) == 1 assert events[0]["eventID"] == 0