iManageThreatManager
iManage Threat Manager protects privileged information against internal and external threat actors using machine learning and user behavior analytics.
Analytics & SIEM · iManage Threat Manager
Details
| ID | iManageThreatManager |
|---|---|
| Provider | iManage |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
| Supported Modules | XSIAM |
README
This integration fetches alerts from iManage Threat Manager, an AI-driven security solution that uses machine learning to detect unusual user behavior, prevent data loss, and ensure compliance.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex.
Configure iManage Threat Manager in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | Should be in format https://<your-instance>.tm-cloudimanage.com | True |
| User Name | Username for user sign-in authentication. Required for Addressable Alerts and Detect And Protect Alerts. | Conditional |
| Password | Password for user sign-in authentication. Required for Addressable Alerts and Detect And Protect Alerts. | Conditional |
| Token | Application token for API token authentication. Required for Behavior Analytics alerts. | Conditional |
| Secret | Application secret for API token authentication. Required for Behavior Analytics alerts. | Conditional |
| Fetch events | Whether to fetch events. | False |
| Events types to fetch | Select which event types to fetch: Behavior Analytics alerts, Addressable Alerts, Detect And Protect Alerts. Default is Behavior Analytics alerts. | False |
| Maximum number of events per type | Default and maximum is 900 events to fetch for each type. | False |
| Trust any certificate (not secure) | Use SSL secure connection or not. | False |
| Use system proxy settings | Use proxy settings for connection or not. | False |
iManage Threat Manager Authentication
The integration supports two authentication methods:
Application Token Authentication (for Behavior Analytics alerts)
To generate an application token and secret from the Threat Manager admin console:
- In iManage Threat Manager, browse to Configuration > System > Application Tokens for Utility Access.
- Select New Token. The New Token dialog opens.
- In the Token Name field, enter a unique name for this application token.
- Select the Export Alert List permission.
- In Token Expiry Time in minutes, enter the number of minutes before this token becomes invalid.
- By default, application tokens expire after 1400 minutes (1 day). The maximum value is 525600 (365 days).
- Select Generate Token.
- The New Token dialog shows the generated application token and secret.
Note: The Integrations Manager role is required to generate an application token. If a user with the Integration Manager role is made inactive or the role is removed, all existing application tokens created by that user become inactive.
User Sign-in Authentication (for Addressable Alerts and Detect And Protect Alerts)
Use your iManage Threat Manager username and password. This provides a similar level of access to what the user would have in the admin console.
Important: These alert types cannot be accessed through application token authentication and require user credentials.
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.
imanage-threat-manager-get-events
Fetches events from iManage Threat Manager. This manual command is used for developing or debugging and should be used with caution, as it can create events, leading to event duplication and exceeding API request limits.
Base Command
imanage-threat-manager-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 and false. The default value is false. | Required |
| event_type | Type of events to fetch. Possible values are: Behavior Analytics alerts, Addressable Alerts, Detect And Protect Alerts. Default is Behavior Analytics alerts. | Optional |
| limit | Maximum number of results to return. Default is 50. | Optional |
| from_date | Start date from which to get events. Supports ISO format or natural language (e.g., “7 days ago”, “1 hour ago”). Default is 1 hour ago. | Optional |
| to_date | End date until which to get events. Supports ISO format or natural language (e.g., “now”, “30 minutes ago”). Default is now. | Optional |
Context Output
There is no context output for this command.
Additional Information
- Timezone: All timestamps are in UTC
Configuration parameters
url— Server URL (required)credentials_user— User Namecredentials_token— Tokeninsecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetchEvents— Fetch eventsevent_types— Events types to fetchmax_events_per_type— Maximum number of events per type
Commands (1)
-
imanage-threat-manager-get-eventsFetches events from iManage Threat Manager.
import base64 import json import pytest from freezegun import freeze_time from iManageThreatManager import ( Client, get_events_command, fetch_events_command, validate_credentials_for_event_types, _add_fields_to_events, _deduplicate_events, _fetch_events_with_pagination, _update_next_run_state, BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS, DETECT_AND_PROTECT_ALERTS, EVENT_TYPE_CONFIG, ) from CommonServerPython import DemistoException BASE_URL = "https://test-instance.tm-cloudimanage.com" @pytest.fixture(autouse=True) def mock_content_client_init(mocker): """Mock ContentClient.__init__ to avoid httpx/anyio initialization in tests.""" mocker.patch("iManageThreatManager.ContentClient.__init__", return_value=None) @pytest.fixture def client(): """ Given: - Base URL and authentication credentials When: - Creating a Client instance Then: - Ensure the client is properly initialized """ c = Client( base_url=BASE_URL, verify=True, proxy=False, username="test_user", password="test_password", token="test_token", secret="test_secret", ) # Set attributes that ContentClient.__init__ would normally set c._base_url = BASE_URL c._verify = True return c class TestValidateCredentialsForEventTypes: """Tests for validate_credentials_for_event_types function""" def test_validate_credentials_behavior_analytics_success(self, client): """ Given: - Client with valid token and secret - Behavior Analytics event type When: - Calling validate_credentials_for_event_types Then: - Ensure no exception is raised """ validate_credentials_for_event_types(client, [BEHAVIOR_ANALYTICS]) def test_validate_credentials_addressable_alerts_success(self, client): """ Given: - Client with valid username and password - Addressable Alerts event type When: - Calling validate_credentials_for_event_types Then: - Ensure no exception is raised """ validate_credentials_for_event_types(client, [ADDRESSABLE_ALERTS]) def test_validate_credentials_missing_token_secret(self): """ Given: - Client without token and secret - Behavior Analytics event type When: - Calling validate_credentials_for_event_types Then: - Ensure DemistoException is raised """ client_no_token = Client(base_url=BASE_URL, verify=True, proxy=False, username="user", password="pass") with pytest.raises(DemistoException, match="Token and Secret"): validate_credentials_for_event_types(client_no_token, [BEHAVIOR_ANALYTICS]) def test_validate_credentials_missing_username_password(self): """ Given: - Client without username and password - Addressable Alerts event type When: - Calling validate_credentials_for_event_types Then: - Ensure DemistoException is raised """ client_no_creds = Client(base_url=BASE_URL, verify=True, proxy=False, token="token", secret="secret") with pytest.raises(DemistoException, match="Username and Password"): validate_credentials_for_event_types(client_no_creds, [ADDRESSABLE_ALERTS]) def test_validate_credentials_multiple_event_types(self, client): """ Given: - Client with all credentials - Multiple event types When: - Calling validate_credentials_for_event_types Then: - Ensure no exception is raised """ validate_credentials_for_event_types(client, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS, DETECT_AND_PROTECT_ALERTS]) @pytest.mark.parametrize( "client_kwargs, event_types, expected_errors", [ pytest.param( {"token": "token", "secret": "secret"}, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS, DETECT_AND_PROTECT_ALERTS], ["Addressable Alerts", "Detect And Protect Alerts"], id="token_only-missing_user_password_for_two_types", ), pytest.param( {"username": "user", "password": "pass"}, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS], ["Token and Secret"], id="user_only-missing_token_secret", ), pytest.param( {}, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS], ["Token and Secret", "Username and Password"], id="no_credentials-all_missing", ), ], ) def test_validate_credentials_mixed_partial(self, client_kwargs, event_types, expected_errors): """ Given: - Client with partial credentials - Multiple event types where some require missing credentials When: - Calling validate_credentials_for_event_types Then: - Ensure DemistoException is raised listing all missing credentials """ client = Client(base_url=BASE_URL, verify=True, proxy=False, **client_kwargs) with pytest.raises(DemistoException) as exc_info: validate_credentials_for_event_types(client, event_types) error_msg = str(exc_info.value) for expected in expected_errors: assert expected in error_msg class TestDeduplicateEvents: """Tests for _deduplicate_events function""" def test_deduplicate_events_no_duplicates(self): """ Given: - Events with unique IDs - Empty last_run_ids When: - Calling _deduplicate_events Then: - Ensure all events are returned """ events = [ {"id": "1", "alert_time": 1000}, {"id": "2", "alert_time": 900}, {"id": "3", "alert_time": 800}, ] result = _deduplicate_events(events, [], 700) assert len(result) == 3 def test_deduplicate_events_with_duplicates(self): """ Given: - Events with some duplicate IDs from last run - All events are newer than last_fetch_time When: - Calling _deduplicate_events Then: - Ensure all events are returned (newer events bypass ID check) """ events = [ {"id": "1", "alert_time": 1000}, {"id": "2", "alert_time": 900}, {"id": "3", "alert_time": 800}, ] last_run_ids = ["2"] # All events are newer than 700, so ID check is bypassed result = _deduplicate_events(events, last_run_ids, 700) assert len(result) == 3 def test_deduplicate_events_with_duplicates_same_time(self): """ Given: - Events at or before last_fetch_time - Some IDs are duplicates from last run When: - Calling _deduplicate_events Then: - Ensure duplicates are removed """ events = [ {"id": "1", "alert_time": 700}, {"id": "2", "alert_time": 700}, {"id": "3", "alert_time": 700}, ] last_run_ids = ["2"] result = _deduplicate_events(events, last_run_ids, 700) assert len(result) == 2 assert result[0]["id"] == "1" assert result[1]["id"] == "3" def test_deduplicate_events_newer_than_last_fetch(self): """ Given: - Events newer than last_fetch_time When: - Calling _deduplicate_events Then: - Ensure all newer events are returned without checking IDs """ events = [ {"id": "1", "alert_time": 1000}, {"id": "2", "alert_time": 900}, ] result = _deduplicate_events(events, ["1", "2"], 800) assert len(result) == 2 def test_deduplicate_events_without_ids(self): """ Given: - Events without ID field When: - Calling _deduplicate_events Then: - Ensure events are kept (cannot deduplicate) """ events = [ {"alert_time": 1000}, {"alert_time": 900}, ] result = _deduplicate_events(events, [], 700) assert len(result) == 2 class TestUpdateNextRunState: """Tests for _update_next_run_state function""" def test_no_new_events_keeps_old_state(self): """ Given: - Empty events list - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure old timestamp and IDs are preserved """ events = [] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert new_ids == ["id1", "id2"] def test_new_events_with_newer_timestamp_replaces_ids(self): """ Given: - Events with timestamp newer than last_fetch_time - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure timestamp is updated and old IDs are replaced with new ones """ events = [ {"id": "new1", "alert_time": 2000}, {"id": "new2", "alert_time": 2000}, {"id": "old1", "alert_time": 1500}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 2000 assert set(new_ids) == {"new1", "new2"} assert "id1" not in new_ids assert "id2" not in new_ids def test_new_events_with_same_timestamp_combines_ids(self): """ Given: - Events with same timestamp as last_fetch_time - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure timestamp stays same and old and new IDs are combined """ events = [ {"id": "new1", "alert_time": 1000}, {"id": "new2", "alert_time": 1000}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert set(new_ids) == {"id1", "id2", "new1", "new2"} def test_new_events_with_same_timestamp_avoids_duplicate_ids(self): """ Given: - Events with same timestamp as last_fetch_time - Some IDs overlap with previous run When: - Calling _update_next_run_state Then: - Ensure IDs are combined without duplicates """ events = [ {"id": "id1", "alert_time": 1000}, # Duplicate {"id": "new1", "alert_time": 1000}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert set(new_ids) == {"id1", "id2", "new1"} assert len(new_ids) == 3 # No duplicates def test_older_timestamp_combines_ids(self): """ Given: - Events with timestamp older than last_fetch_time (API doesn't filter by start_date) - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure timestamp is preserved but IDs are combined to prevent duplicate fetches """ events = [ {"id": "old1", "alert_time": 500}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert set(new_ids) == {"id1", "id2", "old1"} class TestAddFieldsToEvents: """Tests for _add_fields_to_events function""" def test_add_fields_to_events_with_update_time(self): """ Given: - Events with update_time - Source log type parameter When: - Calling _add_fields_to_events Then: - Ensure _time and _source_log_type fields are added with correct format """ events = [ {"update_time": 1609459200000, "id": "1"}, # 2021-01-01 00:00:00 UTC {"update_time": 1609545600000, "id": "2"}, # 2021-01-02 00:00:00 UTC ] _add_fields_to_events(events, "BehaviorAnalytics") assert "_time" in events[0] assert "_time" in events[1] assert events[0]["_time"] == "2021-01-01T00:00:00Z" assert events[1]["_time"] == "2021-01-02T00:00:00Z" assert events[0]["_source_log_type"] == "BehaviorAnalytics" assert events[1]["_source_log_type"] == "BehaviorAnalytics" def test_add_fields_to_events_entry_status_new(self): """ Given: - Events where update_time equals alert_time When: - Calling _add_fields_to_events Then: - Ensure _ENTRY_STATUS is set to 'new' """ events = [ {"update_time": 1609459200000, "alert_time": 1609459200000, "id": "1"}, ] _add_fields_to_events(events, "BehaviorAnalytics") assert events[0]["_ENTRY_STATUS"] == "new" def test_add_fields_to_events_entry_status_modified(self): """ Given: - Events where update_time is greater than alert_time When: - Calling _add_fields_to_events Then: - Ensure _ENTRY_STATUS is set to 'modified' """ events = [ {"update_time": 1609545600000, "alert_time": 1609459200000, "id": "1"}, ] _add_fields_to_events(events, "BehaviorAnalytics") assert events[0]["_ENTRY_STATUS"] == "modified" def test_add_fields_to_events_without_update_time(self): """ Given: - Events without update_time field - Source log type parameter When: - Calling _add_fields_to_events Then: - Ensure _time field is not added but _source_log_type is added """ events = [{"id": "1"}] _add_fields_to_events(events, "AddressableAlerts") assert events[0].get("_time") is None assert events[0]["_source_log_type"] == "AddressableAlerts" def test_add_fields_to_events_empty_list(self): """ Given: - Empty events list - Source log type parameter When: - Calling _add_fields_to_events Then: - Ensure no error is raised """ events = [] _add_fields_to_events(events, "BehaviorAnalytics") assert events == [] class TestFetchEventsWithPagination: """Tests for _fetch_events_with_pagination function""" def test_pagination_single_page(self, client, mocker): """ Given: - Limit of 50 events - API returns 30 events (less than page size) When: - Calling _fetch_events_with_pagination Then: - Ensure pagination stops after first page """ mocker.patch.object( client, "_fetch_alerts", return_value=[{"id": str(i), "alert_time": 1000 - i} for i in range(30)], ) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 50) assert len(events) == 30 def test_pagination_multiple_pages(self, client, mocker): """ Given: - Limit of 200 events When: - Calling _fetch_events_with_pagination Then: - Ensure _fetch_alerts is called 3 times with page_size: 90, 90, 20 """ # Mock _fetch_alerts to return dummy data mock_fetch = mocker.patch.object( client, "_fetch_alerts", side_effect=[ [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(90)], [{"id": f"2-{i}", "alert_time": 910 - i} for i in range(90)], [{"id": f"3-{i}", "alert_time": 820 - i} for i in range(90)], ], ) _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 200) assert mock_fetch.call_count == 3 # Verify page_size parameter in each call assert mock_fetch.call_args_list[0][0][3] == 90 # First call assert mock_fetch.call_args_list[1][0][3] == 90 # Second call assert mock_fetch.call_args_list[2][0][3] == 20 # Third call def test_pagination_with_deduplication_at_boundary(self, client, mocker): """ Given: - Events with same alert_time at page boundary When: - Calling _fetch_events_with_pagination Then: - Ensure duplicates at boundary are removed """ # First page: last 3 events have alert_time=910 page1 = [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(87)] page1.extend( [ {"id": "dup-1", "alert_time": 910}, {"id": "dup-2", "alert_time": 910}, {"id": "dup-3", "alert_time": 910}, ] ) # Second page: includes the same 3 duplicate events plus new ones page2 = [ {"id": "dup-1", "alert_time": 910}, {"id": "dup-2", "alert_time": 910}, {"id": "dup-3", "alert_time": 910}, ] page2.extend([{"id": f"2-{i}", "alert_time": 909 - i} for i in range(50)]) mocker.patch.object(client, "_fetch_alerts", side_effect=[page1, page2]) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 200) # Should have 90 from page1 + 50 new from page2 = 140 (3 duplicates removed) assert len(events) == 140 # Verify no duplicate IDs event_ids = [e["id"] for e in events] assert len(event_ids) == len(set(event_ids)) def test_pagination_boundary_all_identical_alert_time_and_id(self, client, mocker): """ Given: - All events at page boundary have identical alert_time and id When: - Calling _fetch_events_with_pagination Then: - Ensure no duplicate events are created """ # Page 1: all events at the boundary share the same alert_time page1 = [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(85)] page1.extend([{"id": f"boundary-{i}", "alert_time": 915} for i in range(5)]) # Page 2: starts with the same boundary events (duplicates) plus new events page2 = [{"id": f"boundary-{i}", "alert_time": 915} for i in range(5)] page2.extend([{"id": f"2-{i}", "alert_time": 914 - i} for i in range(30)]) mocker.patch.object(client, "_fetch_alerts", side_effect=[page1, page2]) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 200) # Verify no duplicate IDs event_ids = [e["id"] for e in events] assert len(event_ids) == len( set(event_ids) ), f"Found duplicate IDs: {[eid for eid in event_ids if event_ids.count(eid) > 1]}" # Should have 90 from page1 + 30 new from page2 = 120 (5 duplicates removed) assert len(events) == 120 def test_pagination_stops_when_limit_reached(self, client, mocker): """ Given: - Limit of 100 events When: - Calling _fetch_events_with_pagination Then: - Ensure pagination stops at limit with page_size: 90, 10 """ mock_fetch = mocker.patch.object( client, "_fetch_alerts", side_effect=[ [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(90)], # Page 1: 90 events [{"id": f"2-{i}", "alert_time": 910 - i} for i in range(10)], # Page 2: 10 events ], ) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 100) assert mock_fetch.call_count == 2 assert mock_fetch.call_args_list[0][0][3] == 90 # First call: page_size=90 assert mock_fetch.call_args_list[1][0][3] == 10 # Second call: page_size=10 (remaining) assert len(events) == 100 class TestClient: """Tests for Client class methods""" def test_get_access_token_from_token_secret(self, client, mocker): """ Given: - Valid token and secret When: - Calling get_access_token_from_token_secret Then: - Ensure access token is returned """ # Mock to bypass caching mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") mocker.patch.object(client, "_http_request", return_value={"access_token": "test_access_token"}) token = client.get_access_token_from_token_secret() assert token == "test_access_token" assert client._access_token == "test_access_token" def test_get_access_token_force_new(self, client, mocker): """ Given: - Client with existing cached token - force_new=True parameter When: - Calling get_access_token_from_token_secret with force_new=True Then: - Ensure new token is generated bypassing cache """ # Set existing token client._access_token = "old_token" mocker.patch.object(client, "_cache_token") mocker.patch.object(client, "_http_request", return_value={"access_token": "new_token"}) token = client.get_access_token_from_token_secret(force_new=True) assert token == "new_token" assert client._access_token == "new_token" def test_get_access_token_from_username_password(self, client, mocker): """ Given: - Valid username and password When: - Calling get_access_token_from_username_password Then: - Ensure access token is returned """ mocker.patch.object(client, "_http_request", return_value={"access_token": "test_user_access_token"}) token = client.get_access_token_from_username_password() assert token == "test_user_access_token" assert client._user_access_token == "test_user_access_token" def test_fetch_alerts_with_retry_on_429(self, client, mocker): """ Given: - API returns HTTP 429 (Too Many Requests) on first attempt - API succeeds on second attempt When: - Calling _fetch_alerts Then: - Ensure retry mechanism works and events are returned """ mocker.patch("time.sleep") # Mock sleep to speed up test mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") call_count = [0] def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": f"token_{call_count[0]}"} # Alert endpoint call_count[0] += 1 if call_count[0] == 1: raise DemistoException("Error in API call [429] - Too Many Requests") return {"results": [{"id": "1", "alert_time": 1000}]} mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) alerts = client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) assert len(alerts) == 1 assert call_count[0] == 2 # Should have made 2 attempts def test_fetch_alerts_with_retry_on_401(self, client, mocker): """ Given: - API returns HTTP 401 (Unauthorized - token expired) on first attempt - API succeeds on second attempt with new token When: - Calling _fetch_alerts Then: - Ensure token is regenerated and request succeeds """ mocker.patch("time.sleep") mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") call_count = [0] def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": f"token_{call_count[0]}"} # Alert endpoint call_count[0] += 1 if call_count[0] == 1: raise DemistoException("Error in API call [401] - Unauthorized") return {"results": [{"id": "1", "alert_time": 1000}]} mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) alerts = client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) assert len(alerts) == 1 assert call_count[0] == 2 def test_fetch_alerts_retry_exhausted(self, client, mocker, capfd): """ Given: - API returns HTTP 429 on all attempts When: - Calling _fetch_alerts Then: - Ensure exception is raised after max retries """ mocker.patch("time.sleep") mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": "token"} raise DemistoException("Error in API call [429] - Too Many Requests") mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) with capfd.disabled(), pytest.raises(DemistoException): client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) def test_fetch_alerts_no_retry_on_other_errors(self, client, mocker): """ Given: - API returns HTTP 500 (Server Error) When: - Calling _fetch_alerts Then: - Ensure no retry is attempted and exception is raised immediately """ mocker.patch.object(client, "_get_cached_token", return_value=None) def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": "token"} raise DemistoException("Error in API call [500] - Server Error") mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) with pytest.raises(DemistoException): client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) class TestJwtExtraction: """Tests for _extract_jwt_expiration edge cases""" @pytest.mark.parametrize( "jwt_token, expected_behavior", [ pytest.param( "only.two_parts", "default", id="malformed_jwt_wrong_number_of_parts", ), pytest.param( "a.b.c.d", "default", id="malformed_jwt_too_many_parts", ), pytest.param( "header.!!!invalid_base64!!!.signature", "default", id="invalid_base64_encoding", ), ], ) @freeze_time("2021-01-10T00:00:00Z") def test_extract_jwt_expiration_malformed(self, client, jwt_token, expected_behavior): """ Given: - A malformed JWT token (wrong parts count, invalid base64, or missing exp) When: - Calling _extract_jwt_expiration Then: - Ensure default 30-minute expiry is returned """ result = client._extract_jwt_expiration(jwt_token) # Default is current time + 1800 seconds (30 minutes) # Frozen time: 2021-01-10T00:00:00Z = 1610236800 expected_default = 1610236800 + 1800 assert result == expected_default @freeze_time("2021-01-10T00:00:00Z") def test_extract_jwt_expiration_missing_exp_field(self, client): """ Given: - A valid JWT structure but payload missing 'exp' field When: - Calling _extract_jwt_expiration Then: - Ensure default 30-minute expiry is returned """ # Create a valid JWT with no 'exp' field payload = base64.urlsafe_b64encode(json.dumps({"sub": "user123"}).encode()).decode().rstrip("=") jwt_token = f"header.{payload}.signature" result = client._extract_jwt_expiration(jwt_token) expected_default = 1610236800 + 1800 assert result == expected_default def test_extract_jwt_expiration_valid(self, client): """ Given: - A valid JWT with 'exp' field When: - Calling _extract_jwt_expiration Then: - Ensure the correct expiration timestamp is returned """ exp_time = 1610240400 # Some future timestamp payload = base64.urlsafe_b64encode(json.dumps({"exp": exp_time}).encode()).decode().rstrip("=") jwt_token = f"header.{payload}.signature" result = client._extract_jwt_expiration(jwt_token) assert result == exp_time class TestTokenCaching: """Tests for _cache_token and _get_cached_token edge cases""" @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_expired(self, client, mocker): """ Given: - A cached token that has already expired When: - Calling _get_cached_token Then: - Ensure None is returned (token is not used) """ # Frozen time: 1610236800. Token expired at 1610236700 (in the past) mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "expired_token", "api_token_expiry": 1610236700, }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result is None @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_expires_within_buffer(self, client, mocker): """ Given: - A cached token that expires within the 5-minute buffer (< 300 seconds from now) When: - Calling _get_cached_token Then: - Ensure None is returned (token is considered expired) """ # Frozen time: 1610236800. Token expires at 1610237000 (200 seconds from now, within 300s buffer) mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "almost_expired_token", "api_token_expiry": 1610237000, }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result is None @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_valid(self, client, mocker): """ Given: - A cached token that is still valid (expires well beyond the 5-minute buffer) When: - Calling _get_cached_token Then: - Ensure the cached token is returned """ # Frozen time: 1610236800. Token expires at 1610238800 (2000 seconds from now, well beyond 300s buffer) mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "valid_token", "api_token_expiry": 1610238800, }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result == "valid_token" @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_non_integer_expiry(self, client, mocker): """ Given: - A cached token with a non-integer expiry value in integration context When: - Calling _get_cached_token Then: - Ensure None is returned (invalid expiry treated as expired) """ mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "some_token", "api_token_expiry": "not_an_integer", }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result is None def test_cache_token_empty_token(self, client, mocker): """ Given: - An empty token string When: - Calling _cache_token Then: - Ensure the token is not cached (setIntegrationContext is not called) """ mock_set_context = mocker.patch("demistomock.setIntegrationContext") mocker.patch("demistomock.getIntegrationContext", return_value={}) client._cache_token("", "api_access_token", "api_token_expiry") mock_set_context.assert_not_called() class TestTestModuleCommand: """Tests for test_module_command function""" @freeze_time("2021-01-10T00:00:00Z") def test_test_module_success_behavior_analytics(self, client, mocker): """ Given: - Valid client with token and secret - Behavior Analytics event type When: - Calling test_module_command Then: - Ensure 'ok' is returned """ from iManageThreatManager import test_module_command mocker.patch.object(client, "_fetch_alerts", return_value=[]) result = test_module_command(client, {}, [BEHAVIOR_ANALYTICS]) assert result == "ok" @freeze_time("2021-01-10T00:00:00Z") def test_test_module_success_addressable_alerts(self, client, mocker): """ Given: - Valid client with username and password - Addressable Alerts event type When: - Calling test_module_command Then: - Ensure 'ok' is returned """ from iManageThreatManager import test_module_command mocker.patch.object(client, "_fetch_alerts", return_value=[]) result = test_module_command(client, {}, [ADDRESSABLE_ALERTS]) assert result == "ok" def test_test_module_auth_error(self, client, mocker, capfd): """ Given: - Invalid credentials When: - Calling test_module_command Then: - Ensure authorization error message is returned """ from iManageThreatManager import test_module_command mocker.patch.object( client, "_fetch_alerts", side_effect=DemistoException("Error in API call [401] - Unauthorized"), ) with capfd.disabled(): result = test_module_command(client, {}, [BEHAVIOR_ANALYTICS]) assert "Authorization Error" in result @freeze_time("2021-01-10T00:00:00Z") def test_test_module_calls_fetch_alerts_without_retries(self, client, mocker): """ Given: - Valid client with credentials - Behavior Analytics event type When: - Calling test_module_command Then: - Ensure _fetch_alerts is called with enable_retries=False to avoid long retry delays that cause platform timeouts """ from iManageThreatManager import test_module_command mock_fetch = mocker.patch.object(client, "_fetch_alerts", return_value=[]) test_module_command(client, {}, [BEHAVIOR_ANALYTICS]) mock_fetch.assert_called_once() call_kwargs = mock_fetch.call_args.kwargs assert ( call_kwargs.get("enable_retries") is False ), "test_module_command must call _fetch_alerts with enable_retries=False to prevent timeout" class TestGetEventsCommand: """Tests for get_events_command function""" @freeze_time("2021-01-10T00:00:00Z") def test_get_events_command_behavior_analytics(self, client, mocker): """ Given: - Valid client and Behavior Analytics event type When: - Calling get_events_command Then: - Ensure events are returned """ mocker.patch.object( client, "_fetch_alerts", return_value=[{"id": "1", "alert_time": 1609459200000, "update_time": 1609459200000}], ) events, results = get_events_command(client, {"event_type": BEHAVIOR_ANALYTICS, "limit": "10"}) assert len(events) == 1 assert events[0]["id"] == "1" @freeze_time("2021-01-10T00:00:00Z") def test_get_events_command_with_pagination(self, client, mocker): """ Given: - Limit of 150 events - API returns events in multiple pages When: - Calling get_events_command Then: - Ensure all events are fetched via pagination """ page1 = [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(90)] page2 = [{"id": f"2-{i}", "alert_time": 910 - i} for i in range(60)] mocker.patch.object( client, "_fetch_alerts", side_effect=[page1, page2], ) events, results = get_events_command(client, {"event_type": BEHAVIOR_ANALYTICS, "limit": "150"}) assert len(events) == 150 class TestFetchEventsCommand: """Tests for fetch_events_command function""" @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_first_run(self, client, mocker): """ Given: - Empty last_run (first fetch) - Behavior Analytics event type When: - Calling fetch_events_command Then: - Ensure events are fetched and next_run is set correctly """ # Frozen time: 2021-01-10 00:00:00 UTC = 1610236800000 ms # First fetch lookback: 1 hour = 1610236800000 - 3600000 = 1610233200000 ms # Mock event must be AFTER 1610233200000 to be included mocker.patch.object( client, "_fetch_alerts", return_value=[{"id": "1", "alert_time": 1610235000000, "update_time": 1610235000000}], ) next_run, events = fetch_events_command( client=client, last_run={}, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=10 ) assert len(events) == 1 assert events[0]["_source_log_type"] == EVENT_TYPE_CONFIG[BEHAVIOR_ANALYTICS].source_log_type assert "last_fetch_BehaviorAnalytics" in next_run assert next_run["last_fetch_BehaviorAnalytics"] == 1610235000000 @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_with_pagination(self, client, mocker): """ Given: - max_events_per_type of 200 - API returns events in multiple pages When: - Calling fetch_events_command Then: - Ensure pagination is used to fetch all events """ page1 = [{"id": f"1-{i}", "alert_time": 1000 - i, "update_time": 1000 - i} for i in range(90)] page2 = [{"id": f"2-{i}", "alert_time": 910 - i, "update_time": 910 - i} for i in range(90)] page3 = [{"id": f"3-{i}", "alert_time": 820 - i, "update_time": 820 - i} for i in range(20)] mocker.patch.object( client, "_fetch_alerts", side_effect=[page1, page2, page3], ) next_run, events = fetch_events_command( client=client, last_run={}, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=200 ) assert len(events) == 200 @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_multiple_types(self, client, mocker): """ Given: - Multiple event types configured When: - Calling fetch_events_command Then: - Ensure events from all types are fetched """ behavior_events = [{"id": "1", "alert_time": 1609459200000, "update_time": 1609459200000}] addressable_events = [{"id": "2", "alert_time": 1609545600000, "update_time": 1609545600000}] mocker.patch.object( client, "_fetch_alerts", side_effect=[behavior_events, addressable_events], ) next_run, events = fetch_events_command( client=client, last_run={}, event_types=[BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS], max_events_per_type=10 ) assert len(events) == 2 assert events[0]["_source_log_type"] == EVENT_TYPE_CONFIG[BEHAVIOR_ANALYTICS].source_log_type assert events[1]["_source_log_type"] == EVENT_TYPE_CONFIG[ADDRESSABLE_ALERTS].source_log_type @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_no_new_events(self, client, mocker): """ Given: - Last run with previous fetch time - No new events available When: - Calling fetch_events_command Then: - Ensure empty events list and updated next_run """ mocker.patch.object( client, "_fetch_alerts", return_value=[], ) last_run = {"last_fetch_BehaviorAnalytics": 1609459200000} next_run, events = fetch_events_command( client=client, last_run=last_run, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=10 ) assert len(events) == 0 assert "last_fetch_BehaviorAnalytics" in next_run @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_with_error(self, client, mocker, capfd): """ Given: - API error during fetch When: - Calling fetch_events_command Then: - Ensure error is handled and last_run is preserved """ mocker.patch.object( client, "_fetch_alerts", side_effect=DemistoException("Error in API call [500] - Server Error"), ) last_run = {"last_fetch_BehaviorAnalytics": 1609459200000} with capfd.disabled(): next_run, events = fetch_events_command( client=client, last_run=last_run, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=10 ) # Should preserve last_run on error assert next_run["last_fetch_BehaviorAnalytics"] == 1609459200000 assert len(events) == 0