cybleeventsv2
Cyble Events for Vision Users. Must have Vision API access to use the threat intelligence.
Data Enrichment & Threat Intelligence · CybleEventsV2
Details
| ID | cybleeventsv2 |
|---|---|
| Provider | Cyble |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.2.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Cyble Events for Vision Users. Must have Vision API access to use the threat intelligence.
This integration was integrated and tested with version 2.0 of cybleeventsv2
Configure CybleEventsV2 on Cortex XSOAR
-
Navigate to Settings > Integrations > Servers & Services.
Search for CybleEventsV2.
Click Add instance to create and configure a new integration instance.Parameter Description Required URL Server URL (e.g., https://example.net\) True Access Token Access Token True Collections to Fetch Select collections of incidents to be fetched from the dropdown menu False Severities to Fetch Select severities of incident to be fetched from the dropdown menu False Trust any certificate (not secure) False Use system proxy settings False Incident Fetch Limit Maximum incidents to be fetched every time. Upper limit is 50 incidents False Hide Card Details Select to hide CVV and Expiry date of card False Update Incident to Remote System Select to update changes in any incident to Vision False - To ensure that fetch incidents works:
- Select the Fetches incidents radio button.
- Under Incident type, select Cyble Vision Alert V2.
- Click Test to validate the URLs, token, and connection.
Commands
You can execute these commands from the Cortex XSOAR 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.
cyble-vision-subscribed-services
Get list of Subscribed services
Base Command
cyble-vision-subscribed-services
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| CybleEvents.SubscribedServices | String | A list of subscribed services from Cyble vision |
cyble-vision-fetch-iocs
Fetch the indicators in the given timeline.
Base Command
cyble-vision-fetch-iocs
Input
| Argument Name | Description | Required |
|---|---|---|
| ioc_type | Returns records according to their type (Domain, FileHash-MD5, FileHash-SHA1, FileHash-SHA256, IPv4, IPv6, URL, Email, Wallet-Address). Default is Domain. | Optional |
| ioc | Returns records for the specified indicator value. | Optional |
| from | Returns records that starts from the given page number (the value of the form parameter) in the results list. Default is 1. | Optional |
| limit | Number of records to return (max 100). Using a smaller limit will get faster responses. Default is 1. | Optional |
| sort_by | Sorting based on the column(last_seen,first_seen,ioc_type). Possible values are: last_seen, first_seen, ioc_type. Default is last_seen. | Optional |
| order | Sorting order for ioc either Ascending or Descending based on sort by. Default is desc. | Optional |
| tags | Returns records for the specified tags. | Optional |
| start_date | Timeline start date in the format “YYYY-MM-DD”. Should be used with start_date as timeline range. | Optional |
| end_date | Timeline end date in the format “YYYY-MM-DD”. Should be used with end_date as timeline range. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CybleEvents.IoCs.Data | String | Returns indicator with risk score, confident rating, first seen and last seen |
cyble-vision-fetch-alerts
Fetch alerts based on the given parameters. The alerts would have multiple events grouped into one, based on a specific service type. This way the user will see, in some cases, more events than the limit provides.
Base Command
cyble-vision-fetch-alerts
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | Number of records to return (max 50). Using a smaller limit will get faster responses. Default is 5. | Optional |
| start_date | Timeline start date in the format “%Y-%m-%dT%H:%M:%S%z” (iso-8601). | Required |
| end_date | Timeline end date in the format “%Y-%m-%dT%H:%M:%S%z” (iso-8601). | Required |
| order_by | Sorting order for alert fetch either Ascending or Descending. Possible values are: asc, desc. Default is asc. | Optional |
| from | Returns records for the timeline starting from the given indice. Default is 0. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CybleEvents.Events.name | String | Return Event name |
| CybleEvents.Events.alert_group_id | String | Return alert group id |
| CybleEvents.Events.event_id | String | Return event id |
| CybleEvents.Events.keyword | Unknown | Return keywords |
cyble-vision-fetch-alert-groups
Fetch incident event group
Base Command
cyble-vision-fetch-alert-groups
Input
| Argument Name | Description | Required |
|---|---|---|
| order_by | Sorting order for alert fetch either Ascending or Descending. Possible values are: asc, desc. Default is asc. | Optional |
| limit | Number of records to return (max 50). Using a smaller limit will get faster responses. Default is 5. | Optional |
| start_date | Timeline start date in the format “%Y-%m-%dT%H:%M:%S%z” (iso-8601). | Required |
| end_date | Timeline end date in the format “%Y-%m-%dT%H:%M:%S%z” (iso-8601). | Required |
| from | `Returns records that starts from the given page number (the value of the form parameter) in the results list. Default is 0. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| CybleEvents.AlertGroup | String | Fetch all the alert groups |
update-alert-data
Update the status and/or severity of one or more alerts by ID.
Base Command
cyble-vision-update-alerts
Input
| Argument Name | Description | Required |
|---|---|---|
| ids | The alert ID(s) to update. Comma-separated if multiple. | Required |
| status | The new status to assign to the alert(s). Provide a single value or one per alert, comma-separated. | Optional |
| severity | The new severity to assign to the alert(s). Provide a single value or one per alert, comma-separated. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CybleEvents.AlertUpdate.id | String | The alert ID that was updated. |
| CybleEvents.AlertUpdate.status | String | The updated status. |
| CybleEvents.AlertUpdate.user_severity | String | The updated user severity. |
| CybleEvents.AlertUpdate.service | String | The service associated with the alert. |
Command Example
```bash
!update-alert-data ids=”id1,id2” status=”UNDER_REVIEW,RESOLVED” severity=”HIGH,LOW”
Configuration parameters
base_url— URL (required)credentials— (required)incident_collections— Colletions to fetchincident_severity— Severityinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsmax_fetch— Incident Fetch LimitincidentType— Incident typeincidentFetchInterval— Incidents Fetch IntervalisFetch— Fetch incidentsfirst_fetch_timestamp— First fetch time (by hours)hide_data— Hide Card Detailsmirror— Update Incident to Remote System
Commands (9)
-
cyble-vision-fetch-alert-groupsFetch incident event group.
-
cyble-vision-fetch-alertsFetch alerts based on the given parameters. The alerts would have multiple events grouped into one, based on a specific service type. This way the user will see, in some cases, more events than the limit provides.
-
cyble-vision-fetch-iocsFetch the indicators in the given timeline.
-
cyble-vision-subscribed-servicesGet list of Subscribed services.
-
cyble-vision-update-alertsUpdate multiple Cyble alerts with new status/severity. Service is auto-resolved per alert ID.
-
get-mapping-fieldsRetrieves a User Profile schema, which holds all of the user fields within the application. Used for outgoing-mapping through the Get Schema option.
-
get-modified-remote-dataChecks for incidents modified since the last synchronization time to enable incremental data fetching.
-
get-remote-dataRetrieves the latest data for a specific remote incident by its ID, updating only if modified since the given timestamp.
-
update-remote-systemUpdates alert status/severity on Cyble Vision when mirrored incidents change in Cortex (outgoing mirroring).
""" Enhanced Unit Tests for CybleEventsV2 Integration - """ from CommonServerPython import GetRemoteDataResponse, GetMappingFieldsResponse, CommandResults, DemistoException from CybleEventsV2 import ( Client, cyble_vision_update_alerts_command, get_remote_data_command, manual_fetch, update_remote_system, get_mapping_fields, fetch_subscribed_services_alert, fetch_subscribed_services, cyble_fetch_iocs, set_request, DEFAULT_TAKE_LIMIT, ensure_aware, FETCH_INCIDENT_RETRY_BACKOFF_SECONDS, ) from CommonServerPython import GetModifiedRemoteDataResponse from CybleEventsV2 import check_response def test_test_response_success(mock_client): mock_client._http_request = Mock(return_value={"status": "ok"}) with patch("CybleEventsV2.demisto", demisto_mock): result = check_response(mock_client, "GET", "https://example.com", "dummy-token") assert result == "ok" try: from CybleEventsV2 import ( get_alert_by_id, get_fetch_service_list, get_fetch_severities, cyble_events, get_headers, encode_headers, get_event_format, time_diff_in_mins, build_auth_headers, build_get_alert_payload, ) except ImportError: # If direct import fails, these functions need to be defined in your main module pass import pytest from unittest.mock import Mock, patch, call from datetime import datetime, UTC, timedelta import json try: from CybleEventsV2 import fetch_few_alerts, format_incidents, SAMPLE_ALERTS, INCIDENT_SEVERITY except ImportError: # If imports fail, we'll mock them for now SAMPLE_ALERTS = 10 INCIDENT_SEVERITY = {"low": 1, "medium": 2, "high": 3, "critical": 4} try: from CybleEventsV2 import get_modified_remote_data_command, GetModifiedRemoteDataResponse, MAX_ALERTS HAS_GET_MODIFIED_REMOTE_DATA = True except ImportError: HAS_GET_MODIFIED_REMOTE_DATA = False MAX_ALERTS = 1000 from freezegun import freeze_time import pytz import sys import unittest from CybleEventsV2 import migrate_data, validate_iocs_input, get_alert_payload from CybleEventsV2 import ( get_execution_timeout_seconds, should_stop_before_next_page, should_abort_api_retries, save_events_fetch_checkpoint, parse_events_resume_state, get_current_utc_time, sanitize_token, DEFAULT_EXECUTION_TIMEOUT_SECONDS, ) demisto_mock = Mock() sys.modules["demisto"] = demisto_mock MAX_ALERTS = 100 SAMPLE_ALERTS = 5 LIMIT_EVENT_ITEMS = 1000 # Mock demisto functions globally demisto_mock.debug = Mock() demisto_mock.error = Mock() demisto_mock.params = Mock() # Import your modules # UTC = pytz.UTC def util_load_json(path): with open("test_data/" + path, encoding="utf-8") as f: return json.loads(f.read()) @pytest.fixture def mock_client(): """Fixture to create a mock client""" client = Client(base_url="https://test.com", verify=False) return client @pytest.fixture def sample_alert_payload(): """Fixture for sample alert payload""" return { "id": "alert-123", "service": "compromised_cards", "severity": "high", "status": "UNREVIEWED", "created_at": "2023-04-18T10:00:00Z", "updated_at": "2023-04-18T11:00:00Z", "keyword_name": "test_keyword", "data": {"bank": {"card": {"brand": "Visa", "card_no": "1234-5678-9012-3456", "cvv": "123", "expiry": "12/25"}}}, } @pytest.fixture def sample_ioc_response(): """Fixture for sample IOC response""" return { "iocs": [ { "ioc": "malicious.example.com", "ioc_type": "domain", "first_seen": "2023-04-18T10:00:00Z", "last_seen": "2023-04-18T11:00:00Z", "risk_score": "8.5", "confidence_rating": "high", "sources": ["source1", "source2"], "behaviour_tags": ["malware", "phishing"], "target_countries": ["US", "UK"], "target_regions": ["North America", "Europe"], "target_industries": ["Finance", "Healthcare"], "related_malware": ["trojan1", "ransomware2"], "related_threat_actors": ["apt1", "cybercrime_group"], }, { "ioc": "192.168.1.100", "ioc_type": "ip", "first_seen": "2023-04-17T10:00:00Z", "last_seen": "2023-04-17T11:00:00Z", "risk_score": "7.2", "confidence_rating": "medium", "sources": [], "behaviour_tags": [], "target_countries": [], "target_regions": [], "target_industries": [], "related_malware": [], "related_threat_actors": [], }, ] } def test_get_headers(): api_key = "testkey123" headers = get_headers(api_key) assert headers["Content-Type"] == "application/json" assert headers["Authorization"] == f"Bearer {api_key}" def test_encode_headers(): headers = {"Content-Type": "application/json", "Authorization": "Bearer testkey123"} encoded = encode_headers(headers) for k, v in encoded.items(): assert isinstance(v, bytes) assert v.decode("utf-8") == headers[k] def test_get_event_format(): event = { "name": "Test Event", "severity": "High", "event_id": "evt123", "keyword": "malware", "created_at": "2025-06-04T10:00:00Z", "extra": "ignore this", } formatted = get_event_format(event) assert formatted["name"] == "Test Event" assert formatted["severity"] == "High" assert formatted["event_id"] == "evt123" assert formatted["keyword"] == "malware" assert formatted["created"] == "2025-06-04T10:00:00Z" # rawJSON should be a JSON string of the whole event dict assert json.loads(formatted["rawJSON"]) == event class TestGetRemoteDataCommand: """Test get_remote_data_command function""" @patch("demistomock.debug") @patch("demistomock.error") @patch("CybleEventsV2.get_alert_payload_by_id") def test_get_remote_data_success(self, mock_get_payload, mock_error, mock_debug, mock_client, sample_alert_payload): """Test successful remote data retrieval""" args = {"lastUpdate": "2023-04-18T10:00:00Z", "remoteId": "alert-123"} mock_get_payload.return_value = sample_alert_payload with patch("CybleEventsV2.GetRemoteDataArgs") as mock_args_class: mock_remote_args = Mock() mock_remote_args.remote_incident_id = "alert-123" mock_args_class.return_value = mock_remote_args result = get_remote_data_command(mock_client, "https://test.com", "token", args, [], [], False) assert isinstance(result, GetRemoteDataResponse) assert result.mirrored_object == sample_alert_payload assert result.entries == [] mock_debug.assert_called() @patch("demistomock.debug") @patch("CybleEventsV2.return_error") def test_get_remote_data_invalid_args(self, mock_return_error, mock_debug): """Test get_remote_data_command with invalid arguments""" args = {"invalid": "args"} mock_client = Mock() with patch("CybleEventsV2.GetRemoteDataArgs") as mock_args_class: mock_args_class.side_effect = Exception("Invalid arguments") result = get_remote_data_command(mock_client, "https://test.com", "token", args, [], [], False) assert result is None mock_return_error.assert_called() @patch("demistomock.debug") @patch("demistomock.error") @patch("CybleEventsV2.get_alert_payload_by_id") @patch("CybleEventsV2.return_error") def test_get_remote_data_fetch_failure(self, mock_return_error, mock_get_payload, mock_error, mock_debug, mock_client): """Test get_remote_data_command with fetch failure""" args = {"lastUpdate": "2023-04-18T10:00:00Z", "remoteId": "alert-123"} mock_get_payload.side_effect = Exception("Fetch failed") with patch("CybleEventsV2.GetRemoteDataArgs") as mock_args_class: mock_remote_args = Mock() mock_remote_args.remote_incident_id = "alert-123" mock_args_class.return_value = mock_remote_args result = get_remote_data_command(mock_client, "https://test.com", "token", args, [], [], False) assert result is None mock_error.assert_called() mock_return_error.assert_called() @patch("demistomock.debug") @patch("CybleEventsV2.get_alert_payload_by_id") def test_get_remote_data_no_payload(self, mock_get_payload, mock_debug, mock_client): """Test get_remote_data_command with no payload returned""" args = {"lastUpdate": "2023-04-18T10:00:00Z", "remoteId": "alert-123"} mock_get_payload.return_value = None with patch("CybleEventsV2.GetRemoteDataArgs") as mock_args_class: mock_remote_args = Mock() mock_remote_args.remote_incident_id = "alert-123" mock_args_class.return_value = mock_remote_args result = get_remote_data_command(mock_client, "https://test.com", "token", args, [], [], False) assert isinstance(result, GetRemoteDataResponse) assert result.mirrored_object == {} assert result.entries == [] mock_debug.assert_called_with("[get-remote-data] No incident payload returned") class TestManualFetch: """Test manual_fetch function""" @patch("demistomock.debug") @patch("CybleEventsV2.get_fetch_service_list") @patch("CybleEventsV2.get_fetch_severities") @patch("CybleEventsV2.fetch_few_alerts") @freeze_time("2023-04-19T12:00:00Z") def test_manual_fetch_success(self, mock_fetch_alerts, mock_get_severities, mock_get_services, mock_debug, mock_client): """Test successful manual fetch""" args = {"start_date": "2023-04-18 00:00:00", "end_date": "2023-04-19 00:00:00", "order_by": "desc", "limit": "50"} mock_get_services.return_value = [{"name": "compromised_cards"}] mock_get_severities.return_value = ["HIGH", "MEDIUM"] mock_fetch_alerts.return_value = [{"alert": "data"}] result = manual_fetch(mock_client, args, "token", "https://test.com", [], []) assert result == [{"alert": "data"}] mock_debug.assert_called() mock_fetch_alerts.assert_called_once() @patch("demistomock.debug") @freeze_time("2023-04-19T12:00:00Z") def test_manual_fetch_no_end_date(self, mock_debug, mock_client): """Test manual_fetch with no end_date (should use current time)""" args = {"start_date": "2023-04-18 00:00:00", "order_by": "asc", "limit": "100"} with ( patch("CybleEventsV2.get_fetch_service_list") as mock_get_services, patch("CybleEventsV2.get_fetch_severities") as mock_get_severities, patch("CybleEventsV2.fetch_few_alerts") as mock_fetch_alerts, ): mock_get_services.return_value = [] mock_get_severities.return_value = ["HIGH"] mock_fetch_alerts.return_value = [] result = manual_fetch(mock_client, args, "token", "https://test.com", [], []) call_args = mock_fetch_alerts.call_args[0][1] assert "lte" in call_args assert result == [] def test_manual_fetch_invalid_date_format(self, mock_client): """Test manual_fetch with invalid date format""" args = {"start_date": "invalid-date", "end_date": "2023-04-19 00:00:00"} with pytest.raises(DemistoException, match="Invalid date format"): manual_fetch(mock_client, args, "token", "https://test.com", [], []) @patch("demistomock.debug") @patch("CybleEventsV2.get_fetch_service_list") @patch("CybleEventsV2.get_fetch_severities") @patch("CybleEventsV2.fetch_few_alerts") def test_manual_fetch_default_values( self, mock_fetch_alerts, mock_get_severities, mock_get_services, mock_debug, mock_client ): """Test manual_fetch with default values""" args = {"start_date": "2023-04-18 00:00:00"} mock_get_services.return_value = [] mock_get_severities.return_value = ["HIGH"] mock_fetch_alerts.return_value = [] manual_fetch(mock_client, args, "token", "https://test.com", [], []) call_args = mock_fetch_alerts.call_args[0][1] assert call_args["order_by"] == "asc" assert call_args["take"] == DEFAULT_TAKE_LIMIT @patch("CybleEventsV2.UpdateRemoteSystemArgs") @patch.dict("CybleEventsV2.INCIDENT_STATUS", {"CLOSED": "CLOSED"}) def test_update_remote_system_success(mock_args_class): """Test with valid delta, status, and severity""" mock_client = Mock() mock_parsed_args = Mock() mock_parsed_args.delta = {"status": "CLOSED", "severity": 3} mock_parsed_args.data = {"id": "alert-123", "service": "compromised_cards"} mock_parsed_args.remote_incident_id = None mock_args_class.return_value = mock_parsed_args update_remote_system( mock_client, "PUT", "token", {"delta": {"status": "CLOSED", "severity": 3}, "data": mock_parsed_args.data}, "https://test.com", ) mock_client.update_alert.assert_called_once() alert = mock_client.update_alert.call_args[0][0]["alerts"][0] assert alert["id"] == "alert-123" assert alert["service"] == "compromised_cards" assert alert["status"] == "CLOSED" assert alert["user_severity"] == "HIGH" @patch("CybleEventsV2.UpdateRemoteSystemArgs") def test_update_remote_system_no_delta(mock_args_class): """Should skip update when delta is empty""" mock_client = Mock() mock_parsed_args = Mock() mock_parsed_args.delta = {} mock_parsed_args.data = {"id": "alert-123", "service": "compromised_files"} mock_parsed_args.remote_incident_id = None mock_args_class.return_value = mock_parsed_args result = update_remote_system(mock_client, "PUT", "token", {"delta": {}, "data": mock_parsed_args.data}, "https://test.com") assert result == "alert-123" mock_client.update_alert.assert_not_called() @patch("CybleEventsV2.UpdateRemoteSystemArgs") def test_update_remote_system_partial_data(mock_args_class): """Test with only id and service, no status/severity""" mock_client = Mock() mock_parsed_args = Mock() mock_parsed_args.delta = {"other": "value"} # Unrelated delta mock_parsed_args.data = {"id": "alert-123", "service": "compromised_files"} mock_parsed_args.remote_incident_id = None mock_args_class.return_value = mock_parsed_args result = update_remote_system( mock_client, "PUT", "token", {"delta": {"other": "value"}, "data": mock_parsed_args.data}, "https://test.com" ) assert result == "alert-123" mock_client.update_alert.assert_not_called() @patch("CybleEventsV2.UpdateRemoteSystemArgs") def test_update_remote_system_invalid_severity(mock_args_class): """Severity outside known range should not set user_severity""" mock_client = Mock() mock_parsed_args = Mock() mock_parsed_args.delta = {"severity": 99} mock_parsed_args.data = {"id": "alert-123", "service": "compromised_files"} mock_parsed_args.remote_incident_id = None mock_args_class.return_value = mock_parsed_args result = update_remote_system( mock_client, "PUT", "token", {"delta": {"severity": 99}, "data": mock_parsed_args.data}, "https://test.com" ) assert result == "alert-123" mock_client.update_alert.assert_not_called() class TestGetMappingFields: """Test get_mapping_fields function""" def test_get_mapping_fields_success(self): """Test successful mapping fields retrieval""" result = get_mapping_fields(mock_client, "token", "https://test.com") assert isinstance(result, GetMappingFieldsResponse) class TestFetchSubscribedServicesAlert: """Test fetch_subscribed_services_alert function""" def test_fetch_subscribed_services_success(self, mock_client): """Test successful subscribed services fetch""" mock_services = [{"name": "compromised_cards"}, {"name": "stealer_logs"}, {"name": "darkweb_marketplaces"}] mock_client.get_all_services = Mock(return_value=mock_services) with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "Mocked table output" result = fetch_subscribed_services_alert(mock_client, "GET", "https://test.com", "token") assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybleEvents.ServiceList" assert len(result.outputs) == 3 mock_client.get_all_services.assert_called_once_with("token", "https://test.com") def test_fetch_subscribed_services_empty(self, mock_client): """Test fetch_subscribed_services_alert with empty services""" mock_client.get_all_services = Mock(return_value=[]) with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "Empty table" result = fetch_subscribed_services_alert(mock_client, "GET", "https://test.com", "token") assert isinstance(result, CommandResults) assert len(result.outputs) == 0 class TestCybleFetchIocs: """Test cyble_fetch_iocs function""" @patch("CybleEventsV2.set_request") def test_cyble_fetch_iocs_success(self, mock_set_request, mock_client, sample_ioc_response): """Test successful IOC fetch""" mock_set_request.return_value = sample_ioc_response args = { "ioc": "malicious.example.com", "from": "0", "limit": "100", "sort_by": "risk_score", "order": "desc", "tags": "malware,phishing", "ioc_type": "domain", "start_date": "2023-04-18T00:00:00Z", "end_date": "2023-04-19T00:00:00Z", } with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "IOC table" result = cyble_fetch_iocs(mock_client, "GET", "token", args, "https://test.com") assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybleEvents.IoCs" # Check if outputs exist and is not empty if result.outputs: assert len(result.outputs) >= 1 # Verify API call was made mock_set_request.assert_called_once() # Verify call arguments structure - adjust based on actual function signature call_args = mock_set_request.call_args # The actual parameter access might be different, so let's be more flexible assert call_args is not None @patch("CybleEventsV2.set_request") def test_cyble_fetch_iocs_minimal_args(self, mock_set_request, mock_client): """Test cyble_fetch_iocs with minimal arguments""" mock_set_request.return_value = {"iocs": []} args = {"ioc": "test.com"} with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "Empty IOC table" result = cyble_fetch_iocs(mock_client, "GET", "token", args, "https://test.com") # Verify the function was called and returned a result assert isinstance(result, CommandResults) mock_set_request.assert_called_once() @patch("CybleEventsV2.set_request") def test_cyble_fetch_iocs_api_error(self, mock_set_request, mock_client): """Test cyble_fetch_iocs with API error""" # Mock an API error mock_set_request.side_effect = Exception("API Error") args = {"ioc": "test.com"} with pytest.raises(Exception): cyble_fetch_iocs(mock_client, "GET", "token", args, "https://test.com") @patch("CybleEventsV2.set_request") def test_cyble_fetch_iocs_empty_response(self, mock_set_request, mock_client): """Test cyble_fetch_iocs with empty response""" mock_set_request.return_value = {"iocs": []} args = {"ioc": "test.com"} with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "Empty IOC table" result = cyble_fetch_iocs(mock_client, "GET", "token", args, "https://test.com") assert isinstance(result, CommandResults) # For empty response, outputs might be empty list or None assert result.outputs is not None @patch("CybleEventsV2.set_request") def test_cyble_fetch_iocs_with_valid_response(self, mock_set_request, mock_client): """Test cyble_fetch_iocs handles response correctly""" ioc_response = { "iocs": [ { "ioc": "test.com", "ioc_type": "domain", "first_seen": "2023-04-18T10:00:00Z", "last_seen": "2023-04-18T11:00:00Z", "risk_score": "8.5", "confidence_rating": "high", "sources": "source1,source2", # Might be comma-separated string "behaviour_tags": "tag1,tag2", # Might be comma-separated string "target_countries": "US,UK", "target_regions": "North America", "target_industries": "Finance", "related_malware": "malware1", "related_threat_actors": "actor1", } ] } mock_set_request.return_value = ioc_response args = {"ioc": "test.com"} with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "IOC table" result = cyble_fetch_iocs(mock_client, "GET", "token", args, "https://test.com") assert isinstance(result, CommandResults) if result.outputs: assert len(result.outputs) >= 1 # Check that the IOC data is processed ioc_output = result.outputs[0] assert "ioc" in ioc_output or "IOC" in str(ioc_output) def test_time_diff_in_mins(): start = datetime(2025, 6, 4, 12, 0, 0) end = datetime(2025, 6, 4, 12, 30, 0) # 30 minutes later diff = time_diff_in_mins(start, end) assert diff == 30 # Test zero difference diff_zero = time_diff_in_mins(start, start) assert diff_zero == 0 # Test negative difference if lte < gte diff_negative = time_diff_in_mins(end, start) assert diff_negative == -30 # Test with seconds included start_sec = datetime(2025, 6, 4, 12, 0, 0) end_sec = datetime(2025, 6, 4, 12, 1, 30) # 1 minute 30 seconds later diff_sec = time_diff_in_mins(start_sec, end_sec) assert diff_sec == 1.5 # Additional test fixtures for edge cases @pytest.fixture def malformed_alert_payload(): """Fixture for malformed alert payload""" return { "id": "alert-123", # Missing required fields like service, severity, etc. "some_field": "some_value", } @pytest.fixture def edge_case_ioc_response(): """Fixture for edge case IOC response""" return { "iocs": [ { "ioc": "edge-case.com", "ioc_type": "domain", "first_seen": None, # Null value "last_seen": "", # Empty string "risk_score": "invalid", # Invalid score "confidence_rating": "unknown", "sources": None, "behaviour_tags": [""], # Empty tag "target_countries": ["", "US"], # Mixed empty/valid "target_regions": [], "target_industries": None, "related_malware": [""], "related_threat_actors": [], } ] } # Integration tests for complex scenarios class TestIntegrationScenarios: """Integration tests for complex scenarios""" @patch("CybleEventsV2.set_request") def test_complete_ioc_fetch_workflow(self, mock_set_request, mock_client, sample_ioc_response): """Test complete IOC fetch workflow""" mock_set_request.return_value = sample_ioc_response args = { "ioc": "malicious.example.com", "from": "0", "limit": "50", "sort_by": "risk_score", "order": "desc", "tags": "malware,phishing", "ioc_type": "domain", "start_date": "2023-04-18T00:00:00Z", "end_date": "2023-04-19T00:00:00Z", } with patch("CybleEventsV2.tableToMarkdown") as mock_table: mock_table.return_value = "Complete IOC table" result = cyble_fetch_iocs(mock_client, "GET", "token", args, "https://test.com") # Verify complete processing assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybleEvents.IoCs" assert len(result.outputs) == 2 # Verify data processing for ioc_data in result.outputs: assert "ioc" in ioc_data assert "ioc_type" in ioc_data assert "risk_score" in ioc_data class TestGetFetchServiceList: """Improved unit tests for get_fetch_service_list function""" def test_service_name_mapping_logic(self): """Test the actual service name mapping without mocking the mapping""" mock_client = Mock() test_cases = [ ("Darkweb Marketplaces", "darkweb_marketplaces"), ("Data Breaches", "darkweb_data_breaches"), ("Compromised Endpoints", "stealer_logs"), ("Compromised Cards", "compromised_cards"), ] for display_name, expected_service_name in test_cases: result = get_fetch_service_list(mock_client, [display_name], "url", "token") assert len(result) == 1 assert result[0] == expected_service_name def test_case_insensitive_mapping(self): """Test that service mapping uses exact case match (not case-insensitive).""" mock_client = Mock() # Only use exact-match casing test_cases = ["Darkweb Marketplaces"] for case_variant in test_cases: result = get_fetch_service_list(mock_client, [case_variant], "url", "token") assert result, f"Expected non-empty result for: {case_variant}" assert result[0] == "darkweb_marketplaces", f"Unexpected service name: {result[0]}" def test_all_collections_fetches_from_api(self): """Test that 'All collections' actually calls the API""" mock_client = Mock() expected_services = [ {"name": "service1", "display_name": "Service 1"}, {"name": "service2", "display_name": "Service 2"}, ] mock_client.get_all_services.return_value = expected_services result = get_fetch_service_list(mock_client, ["All collections"], "url", "token") assert result == ["service1", "service2"] mock_client.get_all_services.assert_called_once_with("token", "url") def test_mixed_valid_invalid_collections(self): """Test filtering of valid vs invalid collection names""" mock_client = Mock() mixed_collections = [ "Darkweb Marketplaces", "Invalid Collection", "Data Breaches", "Another Invalid", ] result = get_fetch_service_list(mock_client, mixed_collections, "url", "token") assert len(result) == 2 assert "darkweb_marketplaces" in result assert "darkweb_data_breaches" in result def test_empty_collections_fallback(self): """Test behavior with empty collections (should fetch all)""" mock_client = Mock() mock_client.get_all_services.return_value = [{"name": "default_service"}] result = get_fetch_service_list(mock_client, [], "url", "token") assert result == ["default_service"] mock_client.get_all_services.assert_called_once() def test_duplicate_collections_handling(self): """Test that duplicate collection names are only included once""" mock_client = Mock() duplicate_collections = [ "Darkweb Marketplaces", "Data Breaches", "Darkweb Marketplaces", # Duplicate "Data Breaches", # Duplicate ] result = get_fetch_service_list(mock_client, duplicate_collections, "url", "token") # Result should contain each unique mapped service only once assert sorted(result) == sorted(["darkweb_marketplaces", "darkweb_data_breaches"]) class TestGetFetchSeverities: """Test the actual severity mapping logic""" def test_severity_mapping_accuracy(self): """Test that severity mapping works correctly""" # Mock the function to test the logic without depending on actual implementation with patch("CybleEventsV2.get_fetch_severities") as mock_func: # Test all known severity mappings test_cases = [ (["High"], ["HIGH"]), (["Medium"], ["MEDIUM"]), (["Low"], ["LOW"]), (["Critical"], ["CRITICAL"]), (["High", "Medium"], ["HIGH", "MEDIUM"]), (["low", "HIGH"], ["LOW", "HIGH"]), # Mixed case ] for input_severities, expected_output in test_cases: mock_func.return_value = expected_output result = mock_func(input_severities) assert set(result) == set(expected_output) def test_invalid_severities_filtered(self): """Test that invalid severities are filtered out""" with patch("CybleEventsV2.get_fetch_severities") as mock_func: # Should only contain valid severities expected_output = ["HIGH", "MEDIUM"] mock_func.return_value = expected_output result = mock_func(["High", "InvalidSeverity", "Medium", "AnotherInvalid"]) assert set(result) == set(expected_output) def test_empty_severities_returns_default(self): """Test behavior with empty severity list""" with patch("CybleEventsV2.get_fetch_severities") as mock_func: mock_func.return_value = [] result = mock_func([]) # Should return default severities or empty list assert isinstance(result, list) def test_case_insensitive_severity_mapping(self): """Test that severity mapping is case-insensitive""" with patch("CybleEventsV2.get_fetch_severities") as mock_func: mock_func.return_value = ["HIGH"] case_variants = ["high", "HIGH", "High", "hIgH"] for variant in case_variants: result = mock_func([variant]) assert "HIGH" in result def test_build_auth_headers(): token = "abc123" expected = {"Authorization": "Bearer abc123", "Content-Type": "application/json"} assert build_auth_headers(token) == expected class TestGetModifiedRemoteDataCommandCore: def test_successful_execution_flow(self): client = Mock() client.get_ids_with_retry.return_value = ["incident-001", "incident-002"] with ( patch("CybleEventsV2.get_fetch_service_list", return_value=["service1"]), patch("CybleEventsV2.get_fetch_severities", return_value=["High"]), patch("CybleEventsV2.demisto") as mock_demisto, ): mock_demisto.debug = Mock() mock_demisto.info = Mock() mock_demisto.error = Mock() args = {"lastUpdate": "2024-01-01T00:00:00Z"} result = get_modified_remote_data_command(client, "test_url", "test_token", args, False, ["service1"], ["High"]) assert isinstance(result, GetModifiedRemoteDataResponse) assert result.modified_incident_ids == ["incident-001", "incident-002"] def test_debug_parameter_access(self): client = Mock() client.get_ids_with_retry.return_value = ["test-incident"] class SpyDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.accessed_keys = [] def get(self, key, default=None): self.accessed_keys.append(key) return super().get(key, default) def __getitem__(self, key): self.accessed_keys.append(key) return super().__getitem__(key) spy_args = SpyDict( { "last_update": "2024-01-01T00:00:00Z", "lastUpdate": "2024-01-01T00:00:00Z", } ) with ( patch("CybleEventsV2.get_fetch_service_list", return_value=["service1"]), patch("CybleEventsV2.get_fetch_severities", return_value=["High"]), patch("CybleEventsV2.demisto") as mock_demisto, ): mock_demisto.debug = Mock() mock_demisto.info = Mock() mock_demisto.error = Mock() result = get_modified_remote_data_command(client, "test_url", "test_token", spy_args, False, ["service1"], ["High"]) assert isinstance(result, GetModifiedRemoteDataResponse) assert result.modified_incident_ids == ["test-incident"] def test_minimal_args_approach(self): client = Mock() client.get_ids_with_retry.return_value = ["test-incident"] with ( patch("CybleEventsV2.get_fetch_service_list", return_value=["service1"]), patch("CybleEventsV2.get_fetch_severities", return_value=["High"]), patch("CybleEventsV2.demisto") as mock_demisto, ): mock_demisto.debug = Mock() mock_demisto.info = Mock() mock_demisto.error = Mock() mock_demisto.results = Mock() mock_demisto.command = Mock(return_value="get-modified-remote-data") test_cases = [ {"last_update": "2024-01-01T00:00:00Z"}, {"lastUpdate": "2024-01-01T00:00:00Z"}, {"last-run": "2024-01-01T00:00:00Z"}, {"Last_Update": "2024-01-01T00:00:00Z"}, ] success = False for args in test_cases: try: result = get_modified_remote_data_command( client, "test_url", "test_token", args, False, ["service1"], ["High"] ) if isinstance(result, GetModifiedRemoteDataResponse): assert result.modified_incident_ids == ["test-incident"] success = True break except SystemExit: continue # skip cases that trigger sys.exit due to invalid args if not success: pytest.fail("All parameter name combinations failed") def test_mock_args_directly_in_function(self): client = Mock() client.get_ids_with_retry.return_value = ["test-incident"] args_mock = Mock() args_mock.get.return_value = "2024-01-01T00:00:00Z" args_mock.__getitem__ = Mock(return_value="2024-01-01T00:00:00Z") args_mock.__contains__ = Mock(return_value=True) with ( patch("CybleEventsV2.get_fetch_service_list", return_value=["service1"]), patch("CybleEventsV2.get_fetch_severities", return_value=["High"]), patch("CybleEventsV2.demisto") as mock_demisto, ): mock_demisto.debug = Mock() mock_demisto.info = Mock() mock_demisto.error = Mock() result = get_modified_remote_data_command(client, "test_url", "test_token", args_mock, False, ["service1"], ["High"]) assert isinstance(result, GetModifiedRemoteDataResponse) assert result.modified_incident_ids == ["test-incident"] def test_inspect_function_source(self): import inspect try: source = inspect.getsource(get_modified_remote_data_command) assert "return" in source except Exception: pytest.skip("Cannot inspect function source") class TestGetAlertById: """Improved unit tests for get_alert_by_id function""" @pytest.fixture def mock_client(self): """Create a mock client for testing""" client = Mock() client._http_request = Mock() return client def test_successful_alert_retrieval(self, mock_client): """Test successful retrieval of alert by ID with minimal mocking""" # Setup alert_id = "test_alert_123" token = "test_token" url = "https://test.com" # Mock only the HTTP response mock_response = { "data": [ { "id": alert_id, "title": "Test Alert", "severity": "high", "status": "UNREVIEWED", "created_at": "2023-04-18T10:00:00Z", } ] } mock_client._http_request.return_value = mock_response # Execute result = get_alert_by_id(mock_client, alert_id, token, url) # Verify actual business logic expected_alert = { "id": alert_id, "title": "Test Alert", "severity": "high", "status": "UNREVIEWED", "created_at": "2023-04-18T10:00:00Z", } assert result == expected_alert # Verify HTTP request was made correctly mock_client._http_request.assert_called_once() call_kwargs = mock_client._http_request.call_args.kwargs assert call_kwargs["method"] == "POST" assert "/alerts" in call_kwargs["url_suffix"] assert "Authorization" in call_kwargs["headers"] assert call_kwargs["headers"]["Authorization"] == f"Bearer {token}" def test_alert_not_found_empty_data(self, mock_client): """Test behavior when API returns empty data array""" alert_id = "nonexistent_alert" token = "test_token" url = "https://test.com" # Mock empty response mock_client._http_request.return_value = {"data": []} result = get_alert_by_id(mock_client, alert_id, token, url) assert result is None def test_alert_not_found_no_data_key(self, mock_client): """Test behavior when API response has no 'data' key""" alert_id = "test_alert" mock_client._http_request.return_value = {"status": "success", "message": "No data"} result = get_alert_by_id(mock_client, alert_id, "token", "url") assert result is None def test_multiple_alerts_returned_takes_first(self, mock_client): """Test behavior when multiple alerts are returned (should take first)""" alert_id = "test_alert" mock_response = { "data": [ {"id": alert_id, "title": "First Alert", "severity": "high"}, {"id": alert_id, "title": "Second Alert", "severity": "low"}, ] } mock_client._http_request.return_value = mock_response result = get_alert_by_id(mock_client, alert_id, "token", "url") # Should return the first alert assert result["title"] == "First Alert" assert result["severity"] == "high" def test_malformed_response_handling(self, mock_client): """Test handling of malformed API responses""" # Test various malformed responses malformed_responses = [ None, {}, {"data": None}, {"data": "not_a_list"}, {"data": [None]}, {"data": [{}]}, # Empty alert object ] with patch("CybleEventsV2.get_alert_by_id") as mock_get_alert: for malformed_response in malformed_responses: mock_client._http_request.return_value = malformed_response # Mock the function to return None for malformed responses mock_get_alert.return_value = None # Suppress stdout to avoid test output interference with patch("sys.stdout"): result = mock_get_alert(mock_client, "alert_id", "token", "url") # The function should handle malformed responses gracefully assert result is None @patch("CybleEventsV2.get_alert_by_id") @patch("CybleEventsV2.Client") def test_update_single_alert(mock_client_class, mock_get_alert): args = {"ids": "id1", "status": "UNDER_REVIEW", "severity": "HIGH"} mock_url = "https://example.com" mock_token = "dummy-token" mock_client = Mock() mock_client_class.return_value = mock_client mock_get_alert.return_value = {"id": "id1", "service": "mock_service"} mock_client.update_alert.return_value = {"message": "Success"} result = cyble_vision_update_alerts_command(mock_client, mock_url, mock_token, args) assert isinstance(result, CommandResults) assert len(result.outputs) == 1 assert result.outputs[0]["id"] == "id1" assert result.outputs[0]["status"] == "UNDER_REVIEW" assert result.outputs[0]["user_severity"] == "HIGH" mock_client.update_alert.assert_called_once() @patch("CybleEventsV2.Client.update_alert") @patch("CybleEventsV2.get_alert_by_id") def test_update_alert_data_success_multiple(mock_get_alert, mock_update_alert): args = {"ids": "id1,id2", "status": "INFORMATIONAL,REMEDIATION_NOT_REQUIRED", "severity": "LOW,HIGH"} mock_url = "https://example.com" mock_token = "dummy-token" # Mock return values for get_alert_by_id mock_get_alert.side_effect = [ {"id": "id1", "service": "mock_service"}, {"id": "id2", "service": "mock_service"}, ] # Mock return value for update_alert mock_update_alert.return_value = {"message": "Success"} # Create Client instance client = Client(base_url=mock_url, headers={"Authorization": f"Bearer {mock_token}"}, verify=False, proxy=False) result = cyble_vision_update_alerts_command(client, mock_url, mock_token, args) assert result.outputs_prefix == "CybleEvents.AlertUpdate" assert result.outputs[0]["id"] == "id1" assert result.outputs[1]["id"] == "id2" def test_get_alert_payload_by_id_success(): """Test successful alert payload retrieval""" # Mock client client = Mock() # Mock alert data mock_alert = {"service": "test_service", "id": "alert_123", "title": "Test Alert"} # Mock format_incidents return mock_incident = {"name": "Test Alert", "severity": 1, "rawJSON": json.dumps(mock_alert)} with ( patch("CybleEventsV2.get_alert_by_id", return_value=mock_alert), patch("CybleEventsV2.format_incidents", return_value=[mock_incident]), patch("CybleEventsV2.demisto") as mock_demisto, ): from CybleEventsV2 import get_alert_payload_by_id result = get_alert_payload_by_id( client=client, alert_id="alert_123", token="test_token", url="test_url", incident_collections={}, incident_severity={}, hide_cvv_expiry=False, ) assert result is not None assert "rawJSON" in result mock_demisto.debug.assert_called() class TestFetchFewAlerts: """Simplified tests for fetch_few_alerts function""" def setup_method(self): """Setup test fixtures""" self.client = Mock() self.client.get_data = Mock() self.base_input_params = {"take": 100, "gte": "2024-01-01T00:00:00Z", "order_by": "desc", "hce": False} self.services = ["threat_intel", "compromised_cards", "stealer_logs"] self.url = "https://test.com" self.token = "test-api-token" def test_successful_fetch_basic(self): """Test basic successful fetch from single service""" mock_alerts = [ { "id": "alert-001", "service": "threat_intel", "severity": "High", "created_at": "2024-01-01T10:00:00Z", "status": "active", } ] mock_response = {"data": mock_alerts} self.client.get_data.return_value = mock_response with ( patch("CybleEventsV2.format_incidents") as mock_format, patch("CybleEventsV2.get_event_format") as mock_get_format, patch("CybleEventsV2.demisto"), ): mock_format.return_value = [{"formatted": "event1"}] mock_get_format.return_value = {"final_format": "event1"} result = fetch_few_alerts(self.client, self.base_input_params.copy(), ["threat_intel"], self.url, self.token) assert len(result) >= 0 self.client.get_data.assert_called_once() mock_format.assert_called_once() def test_multiple_services_behavior(self): """Test that function handles multiple services""" mock_alerts = [{"id": "alert-001", "service": "threat_intel", "severity": "High"}] self.client.get_data.return_value = {"data": mock_alerts} with ( patch("CybleEventsV2.format_incidents") as mock_format, patch("CybleEventsV2.get_event_format") as mock_get_format, patch("CybleEventsV2.demisto"), ): mock_format.return_value = [{"formatted": "event"}] mock_get_format.return_value = {"final": "event"} result = fetch_few_alerts( self.client, self.base_input_params.copy(), ["threat_intel", "compromised_cards"], self.url, self.token ) assert self.client.get_data.call_count >= 1 assert len(result) >= 0 def test_exception_handling(self): """Test that function raises return_error on service failure""" self.client.get_data.side_effect = Exception("Service unavailable") with patch("CybleEventsV2.return_error") as mock_return_error, patch("CybleEventsV2.demisto"): fetch_few_alerts( self.client, self.base_input_params.copy(), ["threat_intel", "compromised_cards"], self.url, self.token ) # Expect 2 error calls — one per service assert mock_return_error.call_count == 2 expected_calls = [ call("[fetch_few_alerts] Failed to fetch data from service threat_intel: Service unavailable"), call("[fetch_few_alerts] Failed to fetch data from service compromised_cards: Service unavailable"), ] mock_return_error.assert_has_calls(expected_calls, any_order=False) def test_invalid_response_handling(self): """Test handling of invalid response data""" self.client.get_data.return_value = {"data": "not_a_list"} with patch("CybleEventsV2.demisto"): try: fetch_few_alerts(self.client, self.base_input_params.copy(), ["threat_intel"], self.url, self.token) pytest.fail("Expected SystemExit due to invalid response, but it did not occur.") except SystemExit as e: assert e.code == 0 def test_hce_parameter_basic(self): """Test basic HCE parameter handling""" mock_alerts = [{"id": "test", "service": "compromised_cards", "severity": "High"}] self.client.get_data.return_value = {"data": mock_alerts} with ( patch("CybleEventsV2.format_incidents") as mock_format, patch("CybleEventsV2.get_event_format") as mock_get_format, patch("CybleEventsV2.demisto"), ): mock_format.return_value = [{"formatted": "event"}] mock_get_format.return_value = {"final": "event"} params_with_hce = self.base_input_params.copy() params_with_hce["hce"] = True _ = fetch_few_alerts(self.client, params_with_hce, ["compromised_cards"], self.url, self.token) mock_format.assert_called_once() def test_empty_services_list(self): """Test behavior with empty services list""" with patch("CybleEventsV2.demisto"): result = fetch_few_alerts(self.client, self.base_input_params.copy(), [], self.url, self.token) assert result == [] self.client.get_data.assert_not_called() class TestFormatIncidents: """Comprehensive tests for format_incidents function""" def setup_method(self): """Setup test fixtures""" self.base_alert = { "id": "alert-001", "service": "threat_intel", "severity": "High", "keyword_name": "malware_detection", "created_at": "2024-01-01T10:00:00Z", "status": "UNREVIEWED", "data": {"threat_type": "malware"}, } def test_basic_incident_formatting(self): """Test basic incident formatting without special cases""" alerts = [self.base_alert] with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" result = format_incidents(alerts, hide_cvv_expiry=False) assert len(result) == 1 incident = result[0] # Verify all required fields assert incident["name"] == "Cyble Vision Alert on threat_intel" assert incident["event_type"] == "threat_intel" assert incident["severity"] == INCIDENT_SEVERITY.get("high") assert incident["event_id"] == "alert-001" assert incident["keyword"] == "malware_detection" assert incident["created_at"] == "2024-01-01T10:00:00Z" assert incident["status"] == "Unreviewed" assert incident["mirrorInstance"] == "test-instance" # Verify data_message is JSON string assert isinstance(incident["data_message"], str) parsed_data = json.loads(incident["data_message"]) assert parsed_data == {"threat_type": "malware"} def test_compromised_cards_service_formatting(self): """Test specific formatting for compromised_cards service""" card_alert = { "id": "card-001", "service": "compromised_cards", "severity": "Critical", "keyword_name": "credit_card_leak", "created_at": "2024-01-01T12:00:00Z", "status": "new", "data": { "bank": { "card": { "brand": "Visa", "card_no": "4111111111111111", "cvv": "123", "expiry": "12/25/2025", "level": "Premium", "type": "Credit", } } }, } with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" # Test without hiding CVV/expiry result = format_incidents([card_alert], hide_cvv_expiry=False) assert len(result) == 1 incident = result[0] # Verify card-specific fields assert incident["card_brand"] == "Visa" assert incident["card_no"] == "4111111111111111" assert incident["card_cvv"] == "123" assert incident["card_expiry"] == "12/25/2025" assert incident["card_level"] == "Premium" assert incident["card_type"] == "Credit" def test_compromised_cards_with_hidden_cvv_expiry(self): """Test compromised_cards with CVV/expiry hiding enabled""" card_alert = { "id": "card-002", "service": "compromised_cards", "severity": "High", "keyword_name": "card_data", "created_at": "2024-01-01T13:00:00Z", "status": "active", "data": { "bank": { "card": { "brand": "MasterCard", "card_no": "5555555555554444", "cvv": "456", "expiry": "06/28/2028", "level": "Standard", "type": "Debit", } } }, } with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" # Test with hiding CVV/expiry result = format_incidents([card_alert], hide_cvv_expiry=True) assert len(result) == 1 incident = result[0] # Verify CVV and expiry are masked in the incident assert incident["card_cvv"] == "xxx" assert incident["card_expiry"] == "xx/xx/xxxx" # Verify the original data was also modified parsed_data = json.loads(incident["data_message"]) assert parsed_data["bank"]["card"]["cvv"] == "xxx" assert parsed_data["bank"]["card"]["expiry"] == "xx/xx/xxxx" def test_stealer_logs_service_formatting(self): """Test specific formatting for stealer_logs service""" stealer_alert = { "id": "stealer-001", "service": "stealer_logs", "severity": "Medium", "keyword_name": "credential_theft", "created_at": "2024-01-01T14:00:00Z", "status": "processed", "data": { "filename": "passwords_chrome.txt", "content": { "Application": "Chrome Browser", "Password": "secretpass123", "URL": "https://example.com/login", "Username": "user@example.com", }, }, } with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" result = format_incidents([stealer_alert], hide_cvv_expiry=False) assert len(result) == 1 incident = result[0] # Verify stealer-specific fields assert incident["application"] == "Chrome Browser" assert incident["password"] == "secretpass123" assert incident["url"] == "https://example.com/login" assert incident["username"] == "user@example.com" assert incident["filename"] == "passwords_chrome.txt" def test_stealer_logs_with_missing_content(self): """Test stealer_logs handling when content is missing""" stealer_alert_no_content = { "id": "stealer-002", "service": "stealer_logs", "severity": "Low", "keyword_name": "log_file", "created_at": "2024-01-01T15:00:00Z", "status": "new", "data": { "filename": "empty_log.txt" # No 'content' field }, } with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" result = format_incidents([stealer_alert_no_content], hide_cvv_expiry=False) assert len(result) == 1 incident = result[0] # Should only have filename, no content fields assert incident["filename"] == "empty_log.txt" assert "application" not in incident assert "password" not in incident assert "url" not in incident assert "username" not in incident def test_multiple_alerts_different_services(self): """Test formatting multiple alerts from different services""" alerts = [ # Threat intel alert { "id": "threat-001", "service": "threat_intel", "severity": "High", "keyword_name": "malware", "created_at": "2024-01-01T10:00:00Z", "status": "active", "data": {"threat": "data"}, }, # Card alert { "id": "card-001", "service": "compromised_cards", "severity": "Critical", "keyword_name": "card_breach", "created_at": "2024-01-01T11:00:00Z", "status": "new", "data": { "bank": { "card": { "brand": "Visa", "card_no": "4111111111111111", "cvv": "123", "expiry": "12/25", "level": "Gold", "type": "Credit", } } }, }, # Stealer alert { "id": "stealer-001", "service": "stealer_logs", "severity": "Medium", "keyword_name": "credentials", "created_at": "2024-01-01T12:00:00Z", "status": "processed", "data": { "filename": "creds.txt", "content": { "Application": "Firefox", "Password": "mypass", "URL": "https://test.com", "Username": "testuser", }, }, }, ] with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" result = format_incidents(alerts, hide_cvv_expiry=False) assert len(result) == 3 # Verify each incident has correct service-specific fields threat_incident = next(i for i in result if i["event_id"] == "threat-001") card_incident = next(i for i in result if i["event_id"] == "card-001") stealer_incident = next(i for i in result if i["event_id"] == "stealer-001") # Threat intel should not have card or stealer fields assert "card_brand" not in threat_incident assert "application" not in threat_incident # Card incident should have card fields assert card_incident["card_brand"] == "Visa" assert "application" not in card_incident # Stealer incident should have stealer fields assert stealer_incident["application"] == "Firefox" assert "card_brand" not in stealer_incident def test_error_handling_for_malformed_alerts(self): """Test error handling for malformed alerts""" malformed_alerts = [ # Missing required fields {"id": "incomplete-001"}, # None values {"id": "null-001", "service": None, "severity": None}, # Wrong data types {"id": "wrong-type-001", "service": 123, "data": "not_dict"}, # Valid alert (should be processed) { "id": "valid-001", "service": "threat_intel", "severity": "High", "keyword_name": "test", "created_at": "2024-01-01T10:00:00Z", "status": "active", "data": {"test": "data"}, }, ] with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" result = format_incidents(malformed_alerts, hide_cvv_expiry=False) # Should only return the valid alert, malformed ones should be skipped # The exact behavior depends on implementation - it might return partial data or skip entirely assert isinstance(result, list) # At least one valid incident should be present valid_incidents = [i for i in result if i.get("event_id") == "valid-001"] assert len(valid_incidents) == 1 def test_severity_mapping(self): """Test severity level mapping""" severities_to_test = ["low", "medium", "high", "critical", "unknown"] with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" for severity in severities_to_test: alert = { "id": f"sev-{severity}", "service": "threat_intel", "severity": severity.title(), # Test case variations "keyword_name": "test", "created_at": "2024-01-01T10:00:00Z", "status": "active", "data": {}, } result = format_incidents([alert], hide_cvv_expiry=False) if result: # If alert was processed successfully incident = result[0] expected_severity = INCIDENT_SEVERITY.get(severity.lower()) assert incident["severity"] == expected_severity def test_large_data_handling(self): """Test handling of alerts with large data payloads""" large_data = {f"field_{i}": f"value_{i}" * 100 for i in range(100)} large_alert = { "id": "large-001", "service": "threat_intel", "severity": "High", "keyword_name": "large_dataset", "created_at": "2024-01-01T10:00:00Z", "status": "active", "data": large_data, } with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.integrationInstance.return_value = "test-instance" result = format_incidents([large_alert], hide_cvv_expiry=False) assert len(result) == 1 incident = result[0] # Verify large data is properly JSON serialized parsed_data = json.loads(incident["data_message"]) assert len(parsed_data) == 100 assert all(f"field_{i}" in parsed_data for i in range(10)) # Check first 10 def test_build_get_alert_payload(): alert_id = "test-alert-id" result = build_get_alert_payload(alert_id) expected = { "filters": {"id": [alert_id]}, "excludes": {"status": ["FALSE_POSITIVE"]}, "orderBy": [{"created_at": "desc"}], "skip": 0, "take": 1, "taggedAlert": False, "withDataMessage": True, "countOnly": False, } assert result == expected def test_migrate_data_success(monkeypatch): """Test migrate_data with successful execution.""" # Patch MAX_THREADS directly monkeypatch.setattr("CybleEventsV2.MAX_THREADS", 2) # Patch demisto mock mock_demisto = Mock() monkeypatch.setattr("CybleEventsV2.demisto", mock_demisto, raising=False) fixed_now = datetime(2023, 1, 1, 12, 0, 0, tzinfo=pytz.UTC) monkeypatch.setattr("CybleEventsV2.get_current_utc_time", lambda: fixed_now) mock_client = Mock() mock_client.get_data_with_retry.side_effect = [ ([{"alert": "test_alert"}], datetime(2023, 1, 1, 13, 0, 0, tzinfo=pytz.UTC), False), ([{"alert": "test_alert"}], datetime(2023, 1, 1, 14, 0, 0, tzinfo=pytz.UTC), False), ] input_params = { "services": ["service1", "service2"], "gte": "2023-01-01T00:00:00", "lte": "2023-01-01T23:59:59", "severity": ["HIGH"], "order_by": "desc", "skip": 0, "take": 10, } result_alerts, result_time, stopped_early = migrate_data(mock_client, input_params) assert len(result_alerts) == 2 assert result_alerts[0] == {"alert": "test_alert"} assert isinstance(result_time, datetime) assert result_time == datetime(2023, 1, 1, 14, 0, 0, tzinfo=pytz.UTC) assert stopped_early is False assert mock_client.get_data_with_retry.call_count == 2 # Test validate_iocs_input function @patch("CybleEventsV2.demisto") @patch("CybleEventsV2.arg_to_number") def test_validate_iocs_input_valid_args(mock_arg_to_number, mock_demisto): """Test validate_iocs_input with valid arguments.""" mock_arg_to_number.return_value = 5 args = {"from": "5", "limit": "50", "start_date": "2023-01-01", "end_date": "2023-01-02"} # Should not raise any exception validate_iocs_input(args) # No assertion needed - if no exception is raised, test passes class TestClientMethods(unittest.TestCase): """Unit tests for Client class methods""" def setUp(self): self.client = Client("https://test.com") self.test_url = "https://test.com" self.test_headers = {"Authorization": "Bearer test"} self.test_api_key = "test_api_key_123" self.test_payload = {"key": "value"} self.test_service = "test_service" @patch("CybleEventsV2.get_alert_payload", return_value={"dummy": "payload"}) @patch("CybleEventsV2.demisto") def test_get_data_success(self, mock_demisto, mock_get_alert_payload): input_params = { "url": self.test_url, "api_key": self.test_api_key, } mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"result": "ok"} with patch.object(self.client, "make_request", return_value=mock_response) as mock_make_request: result = self.client.get_data(self.test_service, input_params) assert result == {"result": "ok"} mock_make_request.assert_called_once_with(self.test_url, self.test_api_key, "POST", json.dumps({"dummy": "payload"})) assert mock_demisto.debug.called @patch("CybleEventsV2.get_alert_payload", return_value={"dummy": "payload"}) def test_get_data_raises_when_missing_url_or_api_key(self, _mock_get_alert_payload): with pytest.raises(ValueError, match="Missing required URL or API key"): self.client.get_data(self.test_service, {"api_key": self.test_api_key}) with pytest.raises(ValueError, match="Missing required URL or API key"): self.client.get_data(self.test_service, {"url": self.test_url}) @patch("CybleEventsV2.get_alert_payload", return_value={"dummy": "payload"}) @patch("CybleEventsV2.demisto") def test_get_data_retries_on_non_200_then_success(self, mock_demisto, _mock_get_alert_payload): input_params = {"url": self.test_url, "api_key": self.test_api_key} fail_resp = Mock() fail_resp.status_code = 503 fail_resp.text = "unavailable" ok_resp = Mock() ok_resp.status_code = 200 ok_resp.json.return_value = {"recovered": True} with ( patch.object(self.client, "make_request", side_effect=[fail_resp, ok_resp]) as mock_make, patch("CybleEventsV2.time.sleep") as mock_sleep, ): result = self.client.get_data(self.test_service, input_params) assert result == {"recovered": True} assert mock_make.call_count == 2 mock_sleep.assert_called() @patch("CybleEventsV2.get_alert_payload", return_value={"dummy": "payload"}) @patch("CybleEventsV2.demisto") def test_get_data_retries_on_invalid_json_then_success(self, mock_demisto, _mock_get_alert_payload): input_params = {"url": self.test_url, "api_key": self.test_api_key} bad_json = Mock() bad_json.status_code = 200 bad_json.json.side_effect = ValueError("not json") good_json = Mock() good_json.status_code = 200 good_json.json.return_value = {"parsed": True} with ( patch.object(self.client, "make_request", side_effect=[bad_json, good_json]), patch("CybleEventsV2.time.sleep") as mock_sleep, ): result = self.client.get_data(self.test_service, input_params) assert result == {"parsed": True} mock_sleep.assert_called_once() assert mock_sleep.call_args[0][0] == FETCH_INCIDENT_RETRY_BACKOFF_SECONDS[0] @patch("CybleEventsV2.demisto") def test_get_all_services_retries_on_request_error_then_success(self, mock_demisto): fail_resp = Mock() fail_resp.status_code = 200 fail_resp.json.return_value = {"data": ["a"]} with ( patch.object(self.client, "make_request", side_effect=[ConnectionError("reset"), fail_resp]), patch("CybleEventsV2.time.sleep") as mock_sleep, ): result = self.client.get_all_services(self.test_api_key, self.test_url) assert result == ["a"] mock_sleep.assert_called() @patch("CybleEventsV2.demisto") def test_get_all_services_exhausts_retries_raises(self, mock_demisto): """After 1 initial try + len(backoffs) retries, the last request error is raised.""" attempts = len(FETCH_INCIDENT_RETRY_BACKOFF_SECONDS) + 1 with ( patch.object(self.client, "make_request", side_effect=[ConnectionError("reset")] * attempts), patch("CybleEventsV2.time.sleep") as mock_sleep, pytest.raises(Exception, match="Failed to get services: reset"), ): self.client.get_all_services(self.test_api_key, self.test_url) assert mock_sleep.call_count == len(FETCH_INCIDENT_RETRY_BACKOFF_SECONDS) @patch("CybleEventsV2.demisto") def test_get_all_services_wrong_format_raises_without_backoff(self, mock_demisto): """Wrong response shape after 200 + valid JSON must not use retry backoff.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"wrong_key": []} with ( patch.object(self.client, "make_request", return_value=mock_response) as mock_make, patch("CybleEventsV2.time.sleep") as mock_sleep, pytest.raises(Exception, match="Failed to get services: Wrong Format for services response"), ): self.client.get_all_services(self.test_api_key, self.test_url) mock_make.assert_called_once() mock_sleep.assert_not_called() def test_insert_data_in_cortex_successful_processing(self): test_input_params = { "limit": "10", "hce": False, "gte": "2024-01-01T12:00:00Z", "lte": "2024-01-01T13:00:00Z", } mock_response_with_data = { "data": [ {"id": 1, "created_at": "2024-01-01T12:00:00Z", "alert": "test1"}, {"id": 2, "created_at": "2024-01-01T13:00:00Z", "alert": "test2"}, ], "status": "success", "total": 2, } mock_response_empty = { "data": [], "status": "success", "total": 0, } with ( patch.object(self.client, "get_data", side_effect=[mock_response_with_data, mock_response_empty]), patch("CybleEventsV2.parse_date") as mock_parse_date, patch("CybleEventsV2.format_incidents") as mock_format_incidents, patch("CybleEventsV2.get_event_format") as mock_get_event_format, patch("CybleEventsV2.demisto") as mock_demisto, patch("CybleEventsV2.get_execution_timeout_seconds", return_value=900.0), patch("CybleEventsV2.save_events_fetch_checkpoint"), ): mock_parse_date.return_value = datetime(2024, 1, 1, 13, 0, 0, tzinfo=UTC) mock_format_incidents.return_value = [{"formatted": "incident1"}, {"formatted": "incident2"}] mock_get_event_format.side_effect = [{"final": "event1"}, {"final": "event2"}] mock_demisto.incidents = Mock(return_value=[]) mock_demisto.debug = Mock() mock_demisto.error = Mock() result_incidents, result_time, stopped_early = self.client.insert_data_in_cortex( self.test_service, test_input_params, is_update=False ) assert self.client.get_data.called, "get_data should be called" assert isinstance(result_incidents, list), "result_incidents should be a list" assert isinstance(result_time, datetime), "result_time should be a datetime" assert stopped_early is False if len(mock_response_with_data.get("data", [])) > 0 and mock_format_incidents.called: mock_format_incidents.assert_called() format_call_args = mock_format_incidents.call_args[0][0] assert len(format_call_args) == 2 assert mock_get_event_format.call_count == 2 assert len(result_incidents) == 2 def test_insert_data_in_cortex_pagination_logic(self): test_input_params = { "limit": 5, "hce": True, "gte": "2024-01-01T12:00:00Z", "lte": "2024-01-01T13:00:00Z", } skip_values = [] call_count = 0 def track_skip(service, params, is_update, execution_start=None): nonlocal call_count call_count += 1 skip_values.append(params.get("skip", 0)) if call_count <= 2: return {"data": [{"id": f"test{call_count}", "created_at": "2024-01-01T12:00:00Z"}]} else: return {"data": []} with ( patch.object(self.client, "get_data", side_effect=track_skip), patch("CybleEventsV2.parse_date", return_value=datetime.now(UTC)), patch("CybleEventsV2.format_incidents", return_value=[{"incident": "test"}]), patch("CybleEventsV2.get_event_format", return_value={"formatted": "event"}), patch("CybleEventsV2.demisto"), patch("CybleEventsV2.get_execution_timeout_seconds", return_value=900.0), patch("CybleEventsV2.save_events_fetch_checkpoint"), ): result_incidents, result_time, stopped_early = self.client.insert_data_in_cortex( self.test_service, test_input_params, is_update=False ) assert skip_values == [0, 1, 2] assert stopped_early is False def test_get_all_services_success_and_failure(self): # --- SUCCESS CASE --- mock_response_success = Mock() mock_response_success.status_code = 200 mock_response_success.json.return_value = {"data": ["service1", "service2"]} with patch.object(self.client, "make_request", return_value=mock_response_success): result = self.client.get_all_services(self.test_api_key, self.test_url) assert result == ["service1", "service2"] # --- FAILURE: STATUS CODE --- mock_response_fail_code = Mock() mock_response_fail_code.status_code = 404 mock_response_fail_code.json.return_value = {} with ( patch.object(self.client, "make_request", return_value=mock_response_fail_code), pytest.raises(Exception, match="Failed to get services: Wrong status code: 404"), ): self.client.get_all_services(self.test_api_key, self.test_url) # --- FAILURE: WRONG FORMAT --- mock_response_bad_format = Mock() mock_response_bad_format.status_code = 200 mock_response_bad_format.json.return_value = {"wrong_key": []} with ( patch.object(self.client, "make_request", return_value=mock_response_bad_format), pytest.raises(Exception, match="Failed to get services: Wrong Format for services response"), ): self.client.get_all_services(self.test_api_key, self.test_url) @patch("requests.request") def test_get_response_success(self, mock_request): mock_resp = Mock() mock_resp.raise_for_status = Mock() mock_resp.json.return_value = {"data": {"some": "value"}} mock_request.return_value = mock_resp result = self.client.get_response(self.test_url, self.test_headers, self.test_payload, "GET") assert result == {"some": "value"} mock_request.assert_called_once_with("GET", self.test_url, headers=self.test_headers, params=self.test_payload) @patch("CybleEventsV2.return_error") def test_update_alert_success(self, mock_return_error): mock_response = Mock() mock_response.status_code = 200 with patch.object(self.client, "make_request", return_value=mock_response) as mock_make_request: self.client.update_alert(self.test_payload, self.test_url, self.test_api_key) mock_make_request.assert_called_once_with(self.test_url, self.test_api_key, "PUT", json.dumps(self.test_payload)) mock_return_error.assert_not_called() @patch("CybleEventsV2.requests.request") def test_make_request_success(self, mock_request): payload_json = '{"key": "value"}' params = {"p": 1} method = "POST" mock_response = Mock() mock_response.status_code = 200 mock_request.return_value = mock_response response = self.client.make_request(self.test_url, self.test_api_key, method, payload_json, params) assert response.status_code == 200 mock_request.assert_called_once() called_pos_args = mock_request.call_args[0] called_kwargs = mock_request.call_args[1] assert called_pos_args[0] == method assert called_pos_args[1] == self.test_url assert called_kwargs["data"] == payload_json assert called_kwargs["params"] == params assert "headers" in called_kwargs assert "timeout" in called_kwargs @patch("CybleEventsV2.demisto") @patch("CybleEventsV2.time_diff_in_mins", return_value=60) @patch("CybleEventsV2.parse_date", side_effect=[datetime(2024, 1, 1, 12, 0, 0), datetime(2024, 1, 1, 13, 0, 0)]) def test_get_data_with_retry_success(self, mock_parse, mock_diff, mock_demisto): input_params = {"gte": "2024-01-01T12:00:00Z", "lte": "2024-01-01T13:00:00Z"} service = self.test_service mock_inserted_alerts = [{"id": "abc"}] mock_time = datetime(2024, 1, 1, 13, 0, 0) with patch.object(self.client, "insert_data_in_cortex", return_value=(mock_inserted_alerts, mock_time, False)): result_alerts, result_time, stopped_early = self.client.get_data_with_retry(service, input_params) assert result_alerts == mock_inserted_alerts assert isinstance(result_time, datetime) assert stopped_early is False @patch("CybleEventsV2.demisto") @patch("CybleEventsV2.time_diff_in_mins", return_value=60) @patch("CybleEventsV2.parse_date", side_effect=[datetime(2024, 1, 1, 10, 0, 0), datetime(2024, 1, 1, 11, 0, 0)]) def test_get_ids_with_retry_success(self, mock_parse, mock_diff, mock_demisto): input_params = {"gte": "2024-01-01T10:00:00Z", "lte": "2024-01-01T11:00:00Z"} service = self.test_service mock_response = {"data": [{"id": "id1"}, {"id": "id2"}]} with patch.object(self.client, "get_data", return_value=mock_response): result_ids = self.client.get_ids_with_retry(service, input_params) assert result_ids == ["id1", "id2"] class TestCybleEventsLogical(unittest.TestCase): """Logical unit tests for cyble_events function focusing on business logic""" def setUp(self): """Set up test fixtures""" self.mock_client = Mock() self.method = "GET" self.token = "test_token" self.url = "https://api.example.com" self.base_args = {"order_by": "asc"} self.base_last_run = {} self.collections = ["collection1"] self.severities = ["high"] @patch("CybleEventsV2.manual_fetch") def test_skip_true_calls_manual_fetch(self, mock_manual_fetch): """Test that skip=True triggers manual fetch path""" expected_result = ["manual_alert1", "manual_alert2"] mock_manual_fetch.return_value = expected_result result = cyble_events( self.mock_client, self.method, self.token, self.url, self.base_args, self.base_last_run, False, self.collections, self.severities, skip=True, ) assert result == expected_result mock_manual_fetch.assert_called_once_with( self.mock_client, self.base_args, self.token, self.url, self.collections, self.severities ) class TestCybleEventsFunctions(unittest.TestCase): def setUp(self): self.mock_client = Mock() self.mock_client.get_data_with_retry = Mock() @patch("CybleEventsV2.demisto") def test_migrate_data_empty_services(self, mock_demisto): input_params = {"services": []} result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params) assert result_alerts == [] assert isinstance(result_time, datetime) assert stopped_early is False mock_demisto.debug.assert_called_with("No services found in input_params") @patch("CybleEventsV2.demisto") def test_migrate_data_no_services_key(self, mock_demisto): input_params = {} result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params) assert result_alerts == [] assert isinstance(result_time, datetime) assert stopped_early is False mock_demisto.debug.assert_called_with("No services found in input_params") @patch("CybleEventsV2.demisto") def test_migrate_data_successful_execution(self, mock_demisto): input_params = { "services": ["service1", "service2", "service3"], "gte": "2023-01-01T00:00:00Z", "lte": "2023-01-01T23:59:59Z", } test_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=UTC) self.mock_client.get_data_with_retry.return_value = ([{"alert": "test_alert"}], test_time, False) result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params) assert len(result_alerts) == 3 assert isinstance(result_time, datetime) assert stopped_early is False assert self.mock_client.get_data_with_retry.call_count == 3 @patch("CybleEventsV2.demisto") def test_migrate_data_with_exception(self, mock_demisto): input_params = {"services": ["service1"], "gte": "2023-01-01T00:00:00Z", "lte": "2023-01-01T23:59:59Z"} self.mock_client.get_data_with_retry.side_effect = Exception("Test error") result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params) assert result_alerts == [] assert stopped_early is True mock_demisto.error.assert_called() error_msg = mock_demisto.error.call_args[0][0] assert "Migration failed" in error_msg assert "Traceback" in error_msg or "Test error" in error_msg @patch("CybleEventsV2.demisto") def test_migrate_data_is_update_true(self, mock_demisto): input_params = {"services": ["service1"], "gte": "2023-01-01T00:00:00Z", "lte": "2023-01-01T23:59:59Z"} test_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=UTC) self.mock_client.get_data_with_retry.return_value = ([{"alert": "test_alert"}], test_time, False) result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params, is_update=True) assert len(result_alerts) == 1 assert stopped_early is False call_args = self.mock_client.get_data_with_retry.call_args assert call_args[0][0] == "service1" assert call_args[0][1] == input_params assert call_args[0][2] is True @patch("CybleEventsV2.demisto") def test_migrate_data_stopped_early(self, mock_demisto): input_params = { "services": ["service1", "service2"], "gte": "2023-01-01T00:00:00Z", "lte": "2023-01-01T23:59:59Z", } test_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=UTC) self.mock_client.get_data_with_retry.return_value = ([{"alert": "a"}], test_time, True) result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params) assert len(result_alerts) == 1 assert stopped_early is True assert self.mock_client.get_data_with_retry.call_count == 1 @patch("CybleEventsV2.demisto") def test_migrate_data_resume_from_checkpoint(self, mock_demisto): input_params = { "services": ["service1", "service2", "service3"], "gte": "2023-01-01T00:00:00Z", "lte": "2023-01-01T23:59:59Z", } last_run = { "service": "service2", "service_index": "1", "skip": "25", "event_pull_start_date": "2023-01-01T00:00:00Z", "window_lte": "2023-01-01T23:59:59Z", "chunk_gte": "2023-01-01T12:00:00Z", "chunk_lte": "2023-01-01T13:00:00Z", } test_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=UTC) self.mock_client.get_data_with_retry.return_value = ([{"alert": "resumed"}], test_time, False) result_alerts, result_time, stopped_early = migrate_data(self.mock_client, input_params, last_run) assert stopped_early is False assert self.mock_client.get_data_with_retry.call_count == 2 first_call = self.mock_client.get_data_with_retry.call_args_list[0] assert first_call[0][0] == "service2" assert first_call[0][3]["resume"]["skip"] == 25 @patch("CybleEventsV2.demisto") def test_validate_iocs_input_valid_params(self, mock_demisto): args = {"from": "0", "limit": "50", "start_date": "2023-01-10", "end_date": "2023-01-15"} try: validate_iocs_input(args) except Exception: pytest.fail("validate_iocs_input raised an unexpected exception") @patch("CybleEventsV2.demisto") def test_validate_iocs_input_no_dates(self, mock_demisto): args = {"from": "5", "limit": "25"} try: validate_iocs_input(args) except Exception: pytest.fail("validate_iocs_input raised an unexpected exception") @patch("CybleEventsV2.demisto") def test_validate_iocs_input_exception_handling(self, mock_demisto): args = {"from": "invalid", "limit": "10"} validate_iocs_input(args) mock_demisto.error.assert_called() @patch("CybleEventsV2.ensure_aware") @patch("CybleEventsV2.demisto") def test_get_alert_payload_basic(self, mock_demisto, mock_ensure_aware): service = "test_service" input_params = { "gte": "2023-01-01T00:00:00", "lte": "2023-01-31T23:59:59", "severity": ["HIGH", "MEDIUM"], "order_by": "desc", "skip": 0, "take": 50, } mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01T00:00:00+00:00" mock_ensure_aware.return_value = mock_datetime result = get_alert_payload(service, input_params) assert "filters" in result assert result["filters"]["service"] == [service] assert "created_at" in result["filters"] assert result["filters"]["severity"] == ["HIGH", "MEDIUM"] assert result["skip"] == 0 assert result["take"] == 50 assert result["countOnly"] is False assert result["taggedAlert"] is False assert result["withDataMessage"] is True @patch("CybleEventsV2.ensure_aware") @patch("CybleEventsV2.demisto") def test_get_alert_payload_with_update_true(self, mock_demisto, mock_ensure_aware): service = "test_service" input_params = { "gte": "2023-01-01T00:00:00", "lte": "2023-01-31T23:59:59", "severity": ["HIGH"], "order_by": "asc", "skip": 10, "take": 25, } mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01T00:00:00+00:00" mock_ensure_aware.return_value = mock_datetime result = get_alert_payload(service, input_params, is_update=True) assert "updated_at" in result["filters"] assert "created_at" not in result["filters"] assert result["orderBy"] == [{"updated_at": "asc"}] @patch("CybleEventsV2.demisto") def test_get_alert_payload_exception_handling(self, mock_demisto): service = "test_service" input_params = {} result = get_alert_payload(service, input_params) mock_demisto.error.assert_called() assert result is None SEVERITIES = {"Low": "LOW", "Medium": "MEDIUM", "High": "HIGH"} class TestFunctions(unittest.TestCase): def test_get_fetch_severities_with_specific_severities(self): input_severities = ["Low", "High"] expected = ["LOW", "HIGH"] result = get_fetch_severities(input_severities) assert result == expected def test_get_fetch_severities_with_all_severities(self): input_severities = ["All severities"] expected = ["LOW", "MEDIUM", "HIGH"] result = get_fetch_severities(input_severities) assert result == expected def test_get_fetch_severities_with_empty_list(self): input_severities = [] expected = ["LOW", "MEDIUM", "HIGH"] result = get_fetch_severities(input_severities) assert result == expected def test_fetch_subscribed_services_returns_names(self): mock_client = Mock() mock_client.get_all_services.return_value = [{"name": "Service1"}, {"name": "Service2"}] token = "dummy_token" method = "GET" base_url = "https://example.com" expected = [{"name": "Service1"}, {"name": "Service2"}] result = fetch_subscribed_services(mock_client, method, base_url, token) assert result == expected def test_fetch_subscribed_services_no_services(self): mock_client = Mock() mock_client.get_all_services.return_value = [] token = "dummy_token" method = "GET" base_url = "https://example.com" expected = [] result = fetch_subscribed_services(mock_client, method, base_url, token) assert result == expected def test_set_request_calls_client_get_response(self): mock_client = Mock() method = "GET" token = "fake_token" input_params = {"param": "value"} url = "https://api.example.com/data" mock_client.get_response.return_value = {"result": "success"} result = set_request(mock_client, method, token, input_params, url) mock_client.get_response.assert_called_once_with(url, {"Authorization": "Bearer " + token}, input_params, method) assert result == {"result": "success"} def test_ensure_aware_adds_utc_timezone_if_naive(self): naive_dt = datetime(2025, 6, 3, 12, 0, 0) aware_dt = ensure_aware(naive_dt) assert aware_dt.tzinfo is not None assert aware_dt.tzinfo == pytz.UTC def test_ensure_aware_converts_to_utc_if_aware(self): local_tz = pytz.timezone("US/Eastern") aware_dt = datetime(2025, 6, 3, 12, 0, 0, tzinfo=local_tz) utc_dt = ensure_aware(aware_dt) assert utc_dt.tzinfo == pytz.UTC def test_get_execution_timeout_seconds_default(): with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.callingContext = {"context": {}} assert get_execution_timeout_seconds() == float(DEFAULT_EXECUTION_TIMEOUT_SECONDS) def test_get_execution_timeout_seconds_from_context(): with patch("CybleEventsV2.demisto") as mock_demisto: mock_demisto.callingContext = {"context": {"TimeoutDuration": 180_000_000_000}} assert get_execution_timeout_seconds() == 180.0 def test_should_stop_before_next_page_first_page(): execution_start = get_current_utc_time() assert should_stop_before_next_page(0, 0, execution_start) is False def test_should_stop_before_next_page_low_remaining_budget(): execution_start = get_current_utc_time() - timedelta(seconds=170) with patch("CybleEventsV2.get_execution_timeout_seconds", return_value=180.0), patch("CybleEventsV2.demisto"): assert should_stop_before_next_page(1, 20.0, execution_start) is True def test_should_stop_before_next_page_enough_budget(): execution_start = get_current_utc_time() - timedelta(seconds=30) with patch("CybleEventsV2.get_execution_timeout_seconds", return_value=900.0), patch("CybleEventsV2.demisto"): assert should_stop_before_next_page(1, 20.0, execution_start) is False def test_should_abort_api_retries_true(): execution_start = get_current_utc_time() - timedelta(seconds=160) with patch("CybleEventsV2.get_execution_timeout_seconds", return_value=180.0): assert should_abort_api_retries(execution_start) is True def test_should_abort_api_retries_false(): execution_start = get_current_utc_time() - timedelta(seconds=10) with patch("CybleEventsV2.get_execution_timeout_seconds", return_value=900.0): assert should_abort_api_retries(execution_start) is False def test_sanitize_token_strips_whitespace_and_zero_width(): assert sanitize_token(" abc\u200bdef ") == "abcdef" assert sanitize_token("") == "" def test_get_current_utc_time_is_aware(): now = get_current_utc_time() assert now.tzinfo is not None assert now.utcoffset() == timedelta(0) def test_parse_events_resume_state(): assert parse_events_resume_state({})["service"] is None assert parse_events_resume_state({"service": "stealer_logs", "skip": "10"})["skip"] == 10 assert ( parse_events_resume_state({"service": "stealer_logs", "event_pull_start_date": "2025-12-01T00:00:00Z"})["window_gte"] == "2025-12-01T00:00:00Z" ) assert parse_events_resume_state({"service": "x", "skip": "bad", "service_index": "bad"})["skip"] == 0 assert parse_events_resume_state({"service": "x", "skip": "bad", "service_index": "bad"})["service_index"] == 0 @patch("CybleEventsV2.demisto.setLastRun") def test_save_events_fetch_checkpoint(mock_set_last_run): save_events_fetch_checkpoint( "2025-12-01T00:00:00Z", "2025-12-01T06:00:00Z", "2025-12-01T00:00:00Z", "2025-12-01T00:15:00Z", "stealer_logs", 2, 50, ) mock_set_last_run.assert_called_once_with( { "event_pull_start_date": "2025-12-01T00:00:00Z", "window_lte": "2025-12-01T06:00:00Z", "chunk_gte": "2025-12-01T00:00:00Z", "chunk_lte": "2025-12-01T00:15:00Z", "service": "stealer_logs", "service_index": "2", "skip": "50", } ) @patch("CybleEventsV2.demisto") @patch("CybleEventsV2.Client") @patch("CybleEventsV2.arg_to_number", side_effect=lambda v: int(v) if v not in (None, "") else None) def test_main_uses_arg_to_number_for_max_fetch(mock_arg_to_number, mock_client_cls, mock_demisto): from CybleEventsV2 import main import CybleEventsV2 as mod mock_demisto.params.return_value = { "base_url": "https://example.com", "credentials": {"password": " token "}, "max_fetch": "250", "insecure": True, "proxy": False, "hide_data": False, "incident_collections": [], "incident_severity": [], } mock_demisto.command.return_value = "test-module" mock_demisto.args.return_value = {} mock_client = Mock() mock_client_cls.return_value = mock_client with patch("CybleEventsV2.check_response", return_value="ok"), patch("CybleEventsV2.return_results"): main() assert mod.MAX_ALERTS == 250 mock_arg_to_number.assert_any_call("250") if __name__ == "__main__": pytest.main([__file__, "-v"])