Doppel
Doppel is a Modern Digital Risk Protection Solution, that detects the phishing and brand cyber attacks on the emerging channels. Doppel scans millions of channels online which includes, social media, domains, paid ads, dark web, emerging channels, etc. Doppel can identify the malicious content and cyber threats, and enables their customers to take down the digital risks proactively. The Cortex XSOAR pack for Doppel mirrors the alerts created by Doppel as Cortex XSOAR incidents. The pack also contains the commands to perform different operations on Doppel alerts.
Data Enrichment & Threat Intelligence · Doppel
Details
| ID | Doppel |
|---|---|
| Provider | Doppel |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
README
Overview
Doppel is a Modern Digital Risk Protection Solution, that detects the phishing and brand cyber attacks on the emerging channels. Doppel scans millions of channels online which includes, social media, domains, paid ads, dark web, emerging channels, etc. Doppel can identify the malicious content and cyber threats, and enables their customers to take down the digital risks proactively. The Cortex XSOAR pack for Doppel mirrors the alerts created by Doppel as Cortex XSOAR incidents. The pack also contains the commands to perform different operations on Doppel alerts.
Configure Doppel on Cortex XSOAR
- Navigate to Settings & Info > Settings > Integrations > Instances.
- Search for Doppel.
- Click Add instance to create and configure a new integration instance.
| Parameter | Description | Required |
|---|---|---|
| Doppel Tenant URL | The Doppel server URL that will be used for calling the APIs. | True |
| API Key | The API Key to use for connection with Doppel. | True |
| User API Key | The User API Key (Optional) to use for connection with Doppel. | False |
| Organization Code | Optional organization identifier used when your Doppel environment is organization-scoped. If required by the Doppel API, include the organization code provided by your Doppel administrator. | False |
| Trust Any Certificate (not secure) | When checked, SSL certificate verification is disabled. Use this only when the Doppel endpoint uses a self-signed or untrusted certificate. | False |
| Use System Proxy Settings | When checked, the integration uses the system proxy defined in the XSOAR engine configuration (d1.conf). This is required if the engine routes outbound traffic through a local or organizational proxy. | False |
| Fetch incidents | False | |
| Incidents Fetch Interval | The interval specifies the cadence with which the Doppel alerts be fetched. | True |
| Incident type | False | |
| Mirror Direction | Choose the direction to mirror the incident: None (No mirroring), Incoming (from Doppel to Cortex XSOAR), Outgoing (from Cortex XSOAR to Doppel), Incoming and Outgoing (from/to Cortex XSOAR and Doppel). Cortex XSOAR only parameter. | False |
| First fetch | First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days). | False |
| Fetch incidents timeout | The time limit in seconds for fetch incidents to run. Leave this empty to cancel the timeout limit. | False |
| Number of incidents for each fetch. | Due to API limitations, the maximum is 100. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
- 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.
doppel-get-alert
Retrieves the alert details by ID or entity. Must include either ID or entity.
Base Command
doppel-get-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The ID of the alert to retrieve details for. | Optional |
| entity | The alerted entity to retrieve details for. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Doppel.Alert.id | String | The unique identifier of the alert (e.g., TET-1953421). |
| Doppel.Alert.entity | String | The URL or profile link related to the alert. |
| Doppel.Alert.brand | String | The brand associated with the alert. |
| Doppel.Alert.queue_state | String | The current state of the alert in the queue. |
| Doppel.Alert.entity_state | String | The current state of the alert entity (e.g., active, inactive). |
| Doppel.Alert.severity | String | The severity level of the alert (e.g., low, medium, high). |
| Doppel.Alert.product | String | The product category associated with the alert (e.g., social media). |
| Doppel.Alert.platform | String | The platform on which the alert was generated (e.g., Bluesky). |
| Doppel.Alert.source | String | The source from which the alert was generated (e.g., Analyst Upload). |
| Doppel.Alert.notes | Unknown | Additional notes related to the alert, if any. |
| Doppel.Alert.created_at | Date | The timestamp when the alert was created. |
| Doppel.Alert.doppel_link | String | The link to the alert on the Doppel platform. |
| Doppel.Alert.entity_content | Unknown | Additional content related to the alert entity. |
| Doppel.Alert.audit_logs.timestamp | Date | Timestamp when the audit log entry was created. |
| Doppel.Alert.audit_logs.type | String | The type of audit log entry (e.g., alert_create). |
| Doppel.Alert.audit_logs.value | String | The value of the audit log entry (e.g., needs_review). |
| Doppel.Alert.audit_logs.changed_by | String | The user who made the change, if available. |
| Doppel.Alert.audit_logs.metadata | Unknown | Additional metadata related to the audit log. |
| Doppel.Alert.tags | Unknown | Tags associated with the alert. |
| Doppel.Alert.uploaded_by | String | The user or source who uploaded the alert (e.g., Doppel). |
Command example
!doppel-get-alert id="TST-31"
Context Example
{
"Doppel": {
"Alert": {
"id": "TST-31",
"entity": "http://dummyrul.com",
"brand": "test_brand",
"queue_state": "doppel_review",
"entity_state": "active",
"severity": "medium",
"product": "domains",
"platform": "domains",
"source": "Analyst Upload",
"notes": null,
"created_at": "2024-11-27T06:51:50.357664",
"doppel_link": "https://app.doppel.com/alerts/TST-31222",
"entity_content": {
"root_domain": {
"domain": "dummyrul.com",
"registrar": null,
"ip_address": null,
"country_code": null,
"hosting_provider": null,
"contact_email": null
}
},
"audit_logs": [
{
"timestamp": "2024-11-27T06:51:50.357664",
"type": "alert_create",
"value": "needs_review",
"changed_by": "currentuser@doppel.com",
"metadata": {}
}
],
"tags": [],
"uploaded_by": "currentuser@doppel.com"
}
}
}
Human Readable Output
Alert Details
ID Entity Brand Queue State Entity State Severity Product Platform Source Created At Doppel Link Uploaded By TST-31 http://dummyrul.com test_brand doppel_review active medium domains domains Analyst Upload 2024-11-27T06:51:50.357664 Doppel Link currentuser@doppel.com
doppel-create-alert
Creates an alert for a specified entity. This command requires the entity to be provided in the arguments.
Base Command
doppel-create-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| entity | The entity for which the alert should be created. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Doppel.CreatedAlert.id | String | The unique ID of the alert. |
| Doppel.CreatedAlert.entity | String | The entity URL associated with the alert. |
| Doppel.CreatedAlert.doppel_link | String | The link to view the alert in the Doppel platform. |
Command example
!doppel-create-alert entity="http://example.com"
Context Example
{
"CreatedAlert": {
"id": "TST-1001",
"entity": "http://example.com",
"doppel_link": "https://app.doppel.com/alerts/TST-1001"
}
}
Human Readable Output
Created Alert
ID Entity Doppel Link TST-1001 http://example.com Doppel Link
doppel-update-alert
Updates an alert in the Doppel platform. Either alert_id or entity must be specified.
Base Command
doppel-update-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | The ID of the alert to update. Either alert_id or entity should be specified, not both. |
Optional |
| entity | The entity of the alert to update. Either alert_id or entity should be specified, not both. |
Optional |
| queue_state | Status of which queue the alert is in. Possible values are: doppel_review, actioned, needs_confirmation, monitoring, taken_down, archived. |
Optional |
| entity_state | State of the alert. Possible values are: active, down, parked. |
Optional |
| comment | A comment to add while updating the alert. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Doppel.UpdatedAlert.id | String | Unique identifier for the alert (e.g., TET-1953421). |
| Doppel.UpdatedAlert.entity | String | The URL or identifier associated with the alert. |
| Doppel.UpdatedAlert.brand | String | The brand associated with the alert. |
| Doppel.UpdatedAlert.queue_state | String | The current state of the alert in the queue. |
| Doppel.UpdatedAlert.entity_state | String | The current state of the alert entity. |
| Doppel.UpdatedAlert.severity | String | The severity of the alert (e.g., Low, Medium, High). |
| Doppel.UpdatedAlert.product | String | The product category related to the alert. |
| Doppel.UpdatedAlert.platform | String | The platform associated with the alert (e.g., Bluesky). |
| Doppel.UpdatedAlert.source | String | The source of the alert (e.g., Analyst Upload). |
| Doppel.UpdatedAlert.notes | Unknown | Additional notes regarding the alert. |
| Doppel.UpdatedAlert.created_at | Date | Timestamp when the alert was created. |
| Doppel.UpdatedAlert.doppel_link | String | URL link to the alert in Doppel Vision. |
| Doppel.UpdatedAlert.entity_content | Unknown | Content details of the alert entity. |
| Doppel.UpdatedAlert.audit_logs.timestamp | Date | Timestamp of the audit log entry. |
| Doppel.UpdatedAlert.audit_logs.type | String | The type of audit log entry. |
| Doppel.UpdatedAlert.audit_logs.value | String | The value associated with the audit log entry. |
| Doppel.UpdatedAlert.audit_logs.changed_by | String | The user who changed the alert, or null if system-generated. |
| Doppel.UpdatedAlert.audit_logs.metadata | Unknown | Additional metadata related to the audit log entry. |
| Doppel.UpdatedAlert.tags | Unknown | List of tags associated with the alert. |
| Doppel.UpdatedAlert.uploaded_by | String | The user or system that uploaded the alert (e.g., Doppel). |
Command example
!doppel-update-alert alert_id="TST-31" queue_state="actioned" entity_state="down" comment="Updated due to new findings"
Context Example
{
"Doppel": {
"UpdatedAlert": {
"id": "TST-31",
"entity": "http://dummyrul.com",
"brand": "test_brand",
"queue_state": "actioned",
"entity_state": "down",
"severity": "medium",
"product": "domains",
"platform": "domains",
"source": "Analyst Upload",
"notes": null,
"created_at": "2024-11-27T06:51:50.357664",
"doppel_link": "https://app.doppel.com/alerts/TST-31222",
"entity_content": {
"root_domain": {
"domain": "dummyrul.com",
"registrar": null,
"ip_address": null,
"country_code": null,
"hosting_provider": null,
"contact_email": null
}
},
"audit_logs": [
{
"timestamp": "2024-11-27T06:51:50.357664",
"type": "alert_update",
"value": "actioned",
"changed_by": "currentuser@doppel.com",
"metadata": {}
}
],
"tags": [],
"uploaded_by": "currentuser@doppel.com"
}
}
}
Human Readable Output
Updated Alert Details
ID Entity Brand Queue State Entity State Severity Product Platform Source Created At Doppel Link Uploaded By TST-31 http://dummyrul.com test_brand actioned down medium domains domains Analyst Upload 2024-11-27T06:51:50.357664 Doppel Link currentuser@doppel.com
doppel-create-abuse-alert
Create an alert for the provided value to abuse box. Will fail if the alert value is invalid or is protected.
Base Command
doppel-create-abuse-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| entity | The entity for which the abuse alert should be created. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Doppel.AbuseAlert.message | String | Message indicating the status of the submission process. |
Command example
!doppel-create-abuse-alert entity="http://malicious.com"
Context Example
{
"Doppel": {
"AbuseAlert": {
"message": "Abuse alert created successfully"
}
}
}
Human Readable Output
Abuse Alert Submission
Message Abuse alert created successfully
doppel-get-alerts
Retrieves a list of alerts. The result can be filtered by provided parameters.
Base Command
doppel-get-alerts
Input
| Argument Name | Description | Required |
|---|---|---|
| search_key | Currently only supports search by URL. | Optional |
| queue_state | New queue status to update alert with (ID required). Possible values: actioned, needs_confirmation, doppel_review, monitoring, taken_down, archived. | Optional |
| product | Product category the report belongs to. Possible values: domains, social_media, mobile_apps, ecommerce, crypto, email, paid_ads, telco, darkweb. | Optional |
| created_before | Filter alerts created before a specific time. Use the ISO 8601 format, such as 2020-01-01T00:11:22Z. For durations, enter values like ‘12 hours’ or ‘7 days’. | Optional |
| created_after | Filter alerts created after a specific time. Use the ISO 8601 format, such as 2020-01-01T00:11:22Z. For durations, enter values like ‘12 hours’ or ‘7 days’. | Optional |
| sort_type | The field to sort the reports by. Defaults to date_sourced. Possible values: date_sourced, date_last_actioned. | Optional |
| sort_order | The order to sort the reports by. Defaults to desc. Possible values: asc, desc. | Optional |
| page | Page number for pagination; defaults to 0. | Optional |
| tags | List of tags to filter alerts. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Doppel.GetAlerts.alerts.id | String | The unique ID of the alert. |
| Doppel.GetAlerts.alerts.entity | String | The entity associated with the alert. |
| Doppel.GetAlerts.alerts.brand | String | The brand related to the alert. |
| Doppel.GetAlerts.alerts.queue_state | String | The queue state of the alert. |
| Doppel.GetAlerts.alerts.entity_state | String | The current state of the entity (active/inactive). |
| Doppel.GetAlerts.alerts.severity | String | The severity of the alert. |
| Doppel.GetAlerts.alerts.product | String | The product related to the alert. |
| Doppel.GetAlerts.alerts.platform | String | The platform associated with the alert. |
| Doppel.GetAlerts.alerts.source | String | The source of the alert. |
| Doppel.GetAlerts.alerts.created_at | Date | The timestamp when the alert was created. |
| Doppel.GetAlerts.alerts.doppel_link | String | The link to the alert in the Doppel platform. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.domain | String | The domain associated with the alert. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.registrar | String | The registrar of the domain. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.ip_address | String | The IP address of the domain. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.country_code | String | The country code of the domain. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.hosting_provider | String | The hosting provider for the domain. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.contact_email | String | The contact email of the domain. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.mx_records | Unknown | The MX records associated with the domain. |
| Doppel.GetAlerts.alerts.entity_content.root_domain.nameservers | Unknown | The nameservers associated with the domain. |
| Doppel.GetAlerts.alerts.audit_logs.timestamp | Date | The timestamp of the audit log. |
| Doppel.GetAlerts.alerts.audit_logs.type | String | The type of the audit log. |
| Doppel.GetAlerts.alerts.audit_logs.value | String | The value associated with the audit log. |
| Doppel.GetAlerts.alerts.audit_logs.changed_by | String | The user or system that changed the status. |
| Doppel.GetAlerts.alerts.audit_logs.metadata | Unknown | Additional metadata for the audit log. |
| Doppel.GetAlerts.alerts.tags | Unknown | The tags associated with the alert. |
| Doppel.GetAlerts.alerts.uploaded_by | String | The source or user who uploaded the alert. |
| Doppel.GetAlerts.metadata.count | Integer | The total count of alerts. |
| Doppel.GetAlerts.metadata.page | Integer | The current page number in the results. |
| Doppel.GetAlerts.metadata.total_pages | Integer | The total number of pages for the alerts. |
| Doppel.GetAlerts.metadata.page_size | Integer | The number of alerts per page. |
Command example
!doppel-get-alerts search_key="http://example.com" sort_order="desc" page="1"
Context Example
{
"Doppel": {
"GetAlerts": {
"alerts": [
{
"id": "ALERT-12345",
"entity": "http://example.com",
"brand": "Test Brand",
"queue_state": "doppel_review",
"entity_state": "active",
"severity": "high",
"product": "domains",
"platform": "website",
"source": "Analyst Upload",
"created_at": "2024-11-27T10:20:30Z",
"doppel_link": "https://app.doppel.com/alerts/ALERT-12345",
"entity_content": {
"root_domain": {
"domain": "example.com",
"registrar": "Example Registrar",
"ip_address": "192.168.1.1",
"country_code": "US",
"hosting_provider": "Example Hosting",
"contact_email": "contact@example.com"
}
},
"audit_logs": [
{
"timestamp": "2024-11-27T10:21:00Z",
"type": "alert_create",
"value": "needs_review",
"changed_by": "analyst@example.com",
"metadata": {}
}
],
"tags": ["phishing", "malware"],
"uploaded_by": "analyst@example.com"
}
],
"metadata": {
"count": 1,
"page": 1,
"total_pages": 1,
"page_size": 50
}
}
}
}
Human Readable Output
Alert Details
ID Entity Brand Queue State Entity State Severity Product Platform Source Created At Doppel Link Uploaded By ALERT-12345 http://example.com Test Brand doppel_review active high domains website Analyst Upload 2024-11-27T10:20:30Z Doppel Link analyst@example.com
get-mapping-fields
Returns the list of fields for an incident type.
Base Command
get-mapping-fields
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
There is no context output for this command.
update-remote-system
Pushes local changes to the remote system. If an XSOAR Incident is closed, created via Doppel Integration, then on Doppel Portal the Alert status will be updated to archived status.
Base Command
update-remote-system
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
There is no context output for this command.
get-remote-data
Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.
Base Command
get-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The ticket ID. | Required |
| lastUpdate | Retrieve entries that were created after lastUpdate. | Required |
Context Output
There is no context output for this command.
get-modified-remote-data
Gets the list of incidents that were modified since the last update time. Note that this method is here for debugging purposes. The get-modified-remote-data command is used as part of a Mirroring feature, which is available from version 6.1.
Base Command
get-modified-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| last_update | Retrieve entries that were created after lastUpdate. | Optional |
Context Output
There is no context output for this command.
Incident Mirroring
Note: The fetch-incidents command creates Cortex XSOAR incidents only for newly detected Doppel alerts. Changes made to a Doppel alert after it was fetched (such as queue state, entity state, or severity updates) are not pulled in by fetch. To keep existing incidents up to date with Doppel, enable mirroring and set the Mirror Direction parameter to Incoming or Incoming And Outgoing.
Mirroring In (Doppel → XSOAR)
When incidents are mirrored into Cortex XSOAR from Doppel:
- Any changes in Doppel alerts (mirroring incoming fields) will be reflected in Cortex XSOAR incidents.
Supported Fields
The following fields are mirrored:
- Doppel Queue State – Indicates the queue where the alert is currently assigned.
- Doppel Entity State – Represents the status of the alert.
- Audit Log History – A record of all changes made to the alert.
Mirroring Out (XSOAR → Doppel)
When incidents are mirrored out from Cortex XSOAR to Doppel. Currently, the Mirror-out feature updates the Doppel Alert queue to an archived state upon XSOAR incident closure. We intend to extend this capability in future releases to provide more comprehensive mirroring.
Supported Fields
The following fields are mirrored:
- Doppel Queue State – Indicates the queue where the alert is currently assigned.
- Close Notes – When an incident is closed with close notes, the notes are sent to Doppel and recorded as a comment on the alert.
Data sharing note: Closing an incident sends its analyst-authored close notes to the Doppel platform as an alert comment, where they are visible to anyone with access to the alert in Doppel. If your close notes may contain sensitive internal information, review them before closing, or remove the closeNotes mapping from the Doppel Outgoing_Mapper to disable this behavior.
Configuration Steps
To enable incident mirroring between Cortex XSOAR incidents and Doppel alerts:
- Enable Fetching incidents in your instance configuration.
-
In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:
Option Description None Turns off incident mirroring. Incoming Any changes in Doppel alerts (mirroring incoming fields) will be reflected in Cortex XSOAR incidents. Outgoing Mirroring out will only update queue state in Doppel alerts (outgoing mirrored fields). Incoming And Outgoing Changes in Cortex XSOAR incidents and Doppel alerts will be reflected in both directions.
Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Doppel.
<~PLATFORM>
License Requirements
The following configuration parameters require one of these licenses: Cortex XSIAM or Agentix:
- Fetch incidents
</~PLATFORM>
Configuration parameters
url— Doppel Tenant URL (required)credentials— (required)user_credentials—organization_code— Organization CodeisFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch Interval (required)incidentType— Incident typemirror_direction— Mirror Directionfirst_fetch— First fetchfetch_timeout— Fetch incidents timeoutmax_fetch— Number of incidents for each fetch.insecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (9)
-
doppel-create-abuse-alertCreate an alert for the provided value to abuse box. Will fail if the alert value is invalid or is protected.
-
doppel-create-alertCreates an alert for a specified entity. This command requires the entity to be provided in the arguments.
-
doppel-get-alertRetrieves the alert details by ID or entity. Must include either ID or entity.
-
doppel-get-alertsRetrieves a list of alerts. The result can be filtered by provided parameters.
-
doppel-update-alertUpdates an alert in the Doppel platform. Either 'alert_id' or 'entity' must be specified.
-
get-mapping-fieldsReturns the list of fields for an incident type.
-
get-modified-remote-dataGet the list of incidents that were modified since the last update time. This method is used for debugging purposes. The get-modified-remote-data command is used as part of the Mirroring feature that was introduced in Cortex XSOAR version 6.1.
-
get-remote-dataGet remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.
-
update-remote-systemPushes local changes to the remote system.
from datetime import datetime import json import pytest import demistomock as demisto from unittest.mock import MagicMock from Doppel import ( test_module, fetch_incidents_command, get_remote_data_command, update_remote_system_command, get_mapping_fields_command, doppel_get_alert_command, doppel_update_alert_command, doppel_get_alerts_command, doppel_create_alert_command, doppel_create_abuse_alert_command, get_modified_remote_data_command, format_datetime, _paginated_call_to_get_alerts, _get_last_fetch_datetime, _get_mirroring_fields, _get_remote_updated_incident_data_with_entry, _normalize_entity_content_for_grid, _parse_fetch_timeout, _parse_max_fetch, _incident_alert_id, _alert_to_incident, _xsoar_severity, Client, ) from CommonServerPython import * from CommonServerUserPython import * ALERTS_RESPONSE = [ {"id": "1", "created_at": "2025-02-01T12:00:00.000000Z"}, {"id": "2", "created_at": "2025-02-01T12:05:00.000000Z"}, ] DOPPEL_API_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" MIRROR_DIRECTION = { "None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both", } def util_load_json(path): """Helper function to load JSON data from a file.""" with open(path, encoding="utf-8") as f: return json.loads(f.read()) # Mock function for _http_request def mock_http_request(method, url_suffix, params=None, headers=None, data=None, json_data=None): if url_suffix == "alert": return util_load_json("test_data/get-alert.json") return {} # Mock function to return alerts in the expected format def mock_get_alerts(*args, **kwargs): if kwargs.get("page", 0) > 0: # Simulate an empty response after the first page return {"alerts": []} modified_alerts = [{**alert, "created_at": alert["created_at"].rstrip("Z")} for alert in ALERTS_RESPONSE] return {"alerts": modified_alerts} # Ensure response is a dictionary @pytest.fixture def client(): # Create a mock client client = MagicMock() # Assign the mock function to get_alerts client.get_alerts.side_effect = mock_get_alerts # Mocking fetch single alert (Used in update_remote_system_command) client.get_alert.return_value = {"id": "123", "queue_state": "open", "entity_state": "active"} # Mocking update alert (Used in update_remote_system_command) client.update_alert.return_value = None # Assume update succeeds return client def test_test_module(mocker, client): """ Given: - A mock Client instance When: - Running test_module() to test connectivity Then: - The function should return 'ok' if the API request is successful """ # Mock the _http_request method mocker.patch.object(client, "_http_request", side_effect=mock_http_request) # Pass an empty dictionary `{}` as `args`, not a string result = test_module(client) # Assert the expected output assert result == "ok" def test_fetch_incidents_command(mocker): """ Test the `fetch_incidents_command` function for multiple fetch cycles. """ # Mocking demisto functions mocker.patch.object(demisto, "params", return_value={"max_fetch": 1, "fetch_timeout": "30"}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") mocker.patch.object(demisto, "incidents") # Load mock data mock_alerts = util_load_json("test_data/get-all-alerts.json") # List of alerts from Doppel # Mock `_paginated_call_to_get_alerts` to simulate API responses in different cycles mocker.patch( "Doppel._paginated_call_to_get_alerts", side_effect=[ mock_alerts["alerts"][:50], # First fetch - fill queue mock_alerts["alerts"][50:100], # Second fetch - next batch [], # Third fetch - No new alerts, return remaining [], # Fourth fetch - No new alerts, return empty ], ) # Run test cycles last_run = None incidents_queue = [] # for current_flow in ['first', 'second', 'third', 'forth']: # Mock last run data mocker.patch.object(demisto, "getLastRun", return_value={"last_run": last_run, "incidents_queue": incidents_queue}) # Call function fetch_incidents_command(client=None, args={}) # Verify incidents pushed to XSOAR incidents_pushed = demisto.incidents.call_args[0][0] assert len(incidents_pushed) == 1, "Mismatch in incidents" incident = incidents_pushed[0] assert "name" in incident assert "type" in incident assert "rawJSON" in incident assert incident["name"].startswith("Doppel Alert"), "Incident name format mismatch" assert incident["occurred"] != "", "Occurred timestamp should not be empty" # Verify last run update last_run_data = demisto.setLastRun.call_args[0][0] assert "last_run" in last_run_data, "last_run not updated" # The cursor advances, but no raw incident data may be persisted in lastRun. last_run = last_run_data["last_run"] assert "incidents_queue" not in last_run_data, "lastRun must not store raw incident data" def test_fetch_incidents_timeout(mocker): """ Test the `fetch_incidents_command` function for multiple fetch cycles. """ # Mocking demisto functions mocker.patch.object(demisto, "params", return_value={"max_fetch": 1, "fetch_timeout": "10"}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") mocker.patch.object(demisto, "incidents") # Load mock data mock_alerts = util_load_json("test_data/get-all-alerts.json") # List of alerts from Doppel # Mock `_paginated_call_to_get_alerts` to simulate API responses in different cycles mocker.patch( "Doppel._paginated_call_to_get_alerts", side_effect=[ mock_alerts["alerts"][:50], # First fetch - fill queue mock_alerts["alerts"][50:100], # Second fetch - next batch [], # Third fetch - No new alerts, return remaining [], # Fourth fetch - No new alerts, return empty ], ) # Run test cycles last_run = None incidents_queue = [] # for current_flow in ['first', 'second', 'third', 'forth']: # Mock last run data mocker.patch.object(demisto, "getLastRun", return_value={"last_run": last_run, "incidents_queue": incidents_queue}) # Call function fetch_incidents_command(client=None, args={}) # Verify incidents pushed to XSOAR incidents_pushed = demisto.incidents.call_args[0][0] assert len(incidents_pushed) == 1, "Mismatch in incidents" incident = incidents_pushed[0] assert "name" in incident assert "type" in incident assert "rawJSON" in incident assert incident["name"].startswith("Doppel Alert"), "Incident name format mismatch" assert incident["occurred"] != "", "Occurred timestamp should not be empty" # Verify last run update last_run_data = demisto.setLastRun.call_args[0][0] assert "last_run" in last_run_data, "last_run not updated" # The cursor advances, but no raw incident data may be persisted in lastRun. last_run = last_run_data["last_run"] assert "incidents_queue" not in last_run_data, "lastRun must not store raw incident data" def test_fetch_incidents_max_fetch(mocker): """ Test the `fetch_incidents_command` function for multiple fetch cycles. """ # Mocking demisto functions mocker.patch.object(demisto, "params", return_value={"max_fetch": 3, "fetch_timeout": "30"}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") mocker.patch.object(demisto, "incidents") # Load mock data mock_alerts = util_load_json("test_data/get-all-alerts.json") # List of alerts from Doppel # Mock `_paginated_call_to_get_alerts` to simulate API responses in different cycles mocker.patch( "Doppel._paginated_call_to_get_alerts", side_effect=[ mock_alerts["alerts"][:50], # First fetch - fill queue mock_alerts["alerts"][50:100], # Second fetch - next batch [], # Third fetch - No new alerts, return remaining [], # Fourth fetch - No new alerts, return empty ], ) # Run test cycles last_run = None incidents_queue = [] # for current_flow in ['first', 'second', 'third', 'forth']: # Mock last run data mocker.patch.object(demisto, "getLastRun", return_value={"last_run": last_run, "incidents_queue": incidents_queue}) # Call function fetch_incidents_command(client=None, args={}) # Verify incidents pushed to XSOAR incidents_pushed = demisto.incidents.call_args[0][0] assert len(incidents_pushed) == 3, "Mismatch in incidents" incident = incidents_pushed[0] assert "name" in incident assert "type" in incident assert "rawJSON" in incident assert incident["name"].startswith("Doppel Alert"), "Incident name format mismatch" assert incident["occurred"] != "", "Occurred timestamp should not be empty" # Verify last run update last_run_data = demisto.setLastRun.call_args[0][0] assert "last_run" in last_run_data, "last_run not updated" # The cursor advances, but no raw incident data may be persisted in lastRun. last_run = last_run_data["last_run"] assert "incidents_queue" not in last_run_data, "lastRun must not store raw incident data" def test_fetch_incidents_no_alerts(mocker): """Test fetch_incidents_command when there are no incidents to fetch.""" # Mock Demisto functions mocker.patch.object(demisto, "params", return_value={"max_fetch": 1, "fetch_timeout": "10"}) mocker.patch.object(demisto, "getLastRun", return_value={"last_run": None, "incidents_queue": []}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "info") mocker.patch.object(demisto, "debug") # Create a mock client mock_client = MagicMock() mocker.patch("Doppel._paginated_call_to_get_alerts", return_value=[]) # Simulating no alerts returned fetch_incidents_command(client=mock_client, args={}) fetch_incidents_command(client=None, args={}) # Assertions demisto.incidents.assert_called_with([]) # Ensure no incidents are created demisto.debug.assert_any_call("Doppel - Created 0 incident(s) in XSOAR.") def test_get_remote_data_command(mocker, requests_mock): """ Given: - A remote incident ID and last update timestamp. When: - Running get_remote_data_command to fetch updates. Then: - It returns the relevant incident entity from the remote system with the expected mirroring fields. """ # Mock API response for fetching incident updates requests_mock.get( "https://example.com/api/alerts", json={"data": [{"id": "123456", "status": "updated", "name": "Test Alert"}]} ) # Mock necessary demisto functions mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") mocker.patch.object(demisto, "args", return_value={"id": "123456", "lastUpdate": "2025-01-27T07:55:10.063742"}) mocker.patch.object(demisto, "command", return_value="get-remote-data") mock_get_remote_updated_incident_data_with_entry = mocker.patch( "Doppel._get_remote_updated_incident_data_with_entry", return_value=( { "id": "123456", "status": "updated", "name": "Test Alert", }, [], ), ) # Prepare client mock client = mocker.Mock() # Call the function result = get_remote_data_command(client, demisto.args()) assert result.mirrored_object == {"id": "123456", "status": "updated", "name": "Test Alert"} assert result.entries == [] mock_get_remote_updated_incident_data_with_entry.assert_called_once() demisto.debug.assert_called() def test_get_remote_data_command_rate_limit_exception(mocker, capfd): """ Given: - A remote incident ID and last update timestamp. - A Rate limit exceeded exception is raised during _get_remote_updated_incident_data_with_entry. When: - Running get_remote_data_command to fetch updates. Then: - It returns a GetRemoteDataResponse with the error message in mirrored_object and logs API rate limit. """ mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error", side_effect=demisto.error) mocker.patch.object(demisto, "args", return_value={"id": "123456", "lastUpdate": "2025-01-27T07:55:10.063742"}) mocker.patch.object(demisto, "command", return_value="get-remote-data") mock_get_remote_updated_incident_data_with_entry = mocker.patch( "Doppel._get_remote_updated_incident_data_with_entry", side_effect=Exception("Rate limit exceeded"), ) client = MagicMock() with capfd.disabled(): result = get_remote_data_command(client, demisto.args()) assert result.mirrored_object == {"in_mirror_error": "Rate limit exceeded"} assert result.entries == [] demisto.debug.assert_called_with("API rate limit") mock_get_remote_updated_incident_data_with_entry.assert_called_once() def test_update_remote_system_command(client, mocker): """Closing an XSOAR incident archives the Doppel alert and preserves live entity_state.""" mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") client.get_alert.return_value = { "id": "123", "queue_state": "needs_review", "entity_state": "down", } args = { "data": {"queue_state": "needs_review"}, "delta": {"closeNotes": "Resolved in XSOAR"}, "incidentChanged": True, "remoteId": "123", "status": IncidentStatus.DONE, } result = update_remote_system_command(client, args) assert result == "123" client.get_alert.assert_called_once_with(id="123", entity="") client.update_alert.assert_called_once_with( queue_state="archived", entity_state="down", comment="Resolved in XSOAR", alert_id="123", ) demisto.error.assert_not_called() def test_update_remote_system_incident_not_closed(mocker, capfd): """Test update_remote_system_command when the incident is not closed.""" mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") mocker.patch.object(demisto, "command", return_value="update-remote-system") client = MagicMock() args = { "data": {"queue_state": "active"}, "entries": [], "incidentChanged": True, "remoteId": "123456", "status": IncidentStatus.ACTIVE, } with capfd.disabled(): update_remote_system_command(client, args) demisto.debug.assert_called_with("Incident not closed. Skipping update for remote ID [123456].") client.get_alert.assert_not_called() client.update_alert.assert_not_called() def test_update_remote_system_already_archived_with_comment(client, mocker): """Already-archived alerts still receive close notes when the XSOAR incident is closed.""" mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") client.get_alert.return_value = { "id": "123", "queue_state": "archived", "entity_state": "down", } args = { "data": {}, "delta": {"closeNotes": "Closing note"}, "incidentChanged": True, "remoteId": "123", "status": IncidentStatus.DONE, } assert update_remote_system_command(client, args) == "123" client.update_alert.assert_called_once_with( queue_state="archived", entity_state="down", comment="Closing note", alert_id="123", ) def test_update_remote_system_already_archived_without_comment(client, mocker): """Skip the API call when the alert is already archived and there are no close notes.""" mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") client.get_alert.return_value = { "id": "123", "queue_state": "archived", "entity_state": "down", } args = { "data": {}, "delta": {}, "incidentChanged": True, "remoteId": "123", "status": IncidentStatus.DONE, } assert update_remote_system_command(client, args) == "123" client.update_alert.assert_not_called() def test_normalize_entity_content_for_grid_root_domain(): """Domains entity_content.root_domain becomes a one-row grid list.""" entity_content = { "root_domain": { "domain": "1.com", "registrar": None, "ip_address": None, "mx_records": [], "nameservers": [], } } assert _normalize_entity_content_for_grid(entity_content) == [ { "domain": "1.com", "registrar": None, "ip_address": None, "mx_records": [], "nameservers": [], } ] def test_normalize_entity_content_for_grid_passthrough_list(): """Already-normalized lists are returned unchanged (dict rows only).""" rows = [{"domain": "a.com"}, {"domain": "b.com"}] assert _normalize_entity_content_for_grid(rows) == rows assert _normalize_entity_content_for_grid([]) == [] assert _normalize_entity_content_for_grid(None) == [] def test_normalize_entity_content_for_grid_single_nested_dict(): """Non-domain archetypes ({archetype_key: {fields}}) unwrap the single nested dict.""" entity_content = {"social_media_post": {"full_text": "spam", "num_upvotes": 3}} assert _normalize_entity_content_for_grid(entity_content) == [{"full_text": "spam", "num_upvotes": 3}] def test_normalize_entity_content_for_grid_flat_dict_fallback(): """A flat dict with no nested objects is used as the grid row so no data is dropped.""" entity_content = {"domain": "flat.com", "registrar": "R"} assert _normalize_entity_content_for_grid(entity_content) == [{"domain": "flat.com", "registrar": "R"}] # Ambiguous shapes with multiple nested dicts still return no rows. ambiguous = {"a": {"x": 1}, "b": {"y": 2}} assert _normalize_entity_content_for_grid(ambiguous) == [] def test_alert_to_incident_normalizes_entity_content(): """Fetched incidents put grid-shaped entity_content into rawJSON for the mapper.""" alert = { "id": "TST-3620", "created_at": "2026-07-31T13:18:52.149692", "severity": "medium", "entity_content": {"root_domain": {"domain": "1.com", "registrar": None}}, } incident = _alert_to_incident(alert, {"mirror_direction": "Both"}) raw = json.loads(incident["rawJSON"]) assert raw["entity_content"] == [{"domain": "1.com", "registrar": None}] def test_get_remote_updated_incident_data_normalizes_entity_content(): """Incoming mirror sync also shapes entity_content for the grid field.""" mock_client = MagicMock() mock_client.get_alert.return_value = { "id": "TST-3620", "queue_state": "archived", "entity_content": {"root_domain": {"domain": "1.com"}}, "audit_logs": [], } updated_alert, _entries = _get_remote_updated_incident_data_with_entry(mock_client, "TST-3620", "2025-02-24T14:30:00.120000Z") assert updated_alert is not None assert updated_alert["entity_content"] == [{"domain": "1.com"}] def test_get_mapping_fields_command(client, mocker): """Test get_mapping_fields_command function.""" # Mocking demisto functions using mocker.patch.object mock_debug = mocker.patch.object(demisto, "debug") # Run the function result = get_mapping_fields_command(client, {}) # Assertions assert result is not None, "Result should not be None" assert hasattr(result, "extract_mapping"), "Result should have extract_mapping method" mapping = result.extract_mapping() assert mapping["Doppel Alert"]["queue_state"] == "Queue State of the Doppel Alert" assert mapping["Doppel Alert"]["entity_state"] == "Current state of the alert entity" assert mapping["Doppel Alert"]["doppel_link"] == "Link to the alert in the Doppel platform" assert mapping["Doppel Alert"]["entity_content"] == "Additional content related to the alert entity" mock_debug.assert_called() # Ensure debug logs are generated def test_get_mapping_fields_command_raises_exception(mocker): """Test get_mapping_fields_command function when an exception occurs.""" # Mock the SchemeTypeMapping to raise an exception mock_scheme = mocker.patch("Doppel.SchemeTypeMapping") mock_scheme.return_value.add_field.side_effect = Exception("Unexpected Error") # Run the function and verify it raises an exception with pytest.raises(Exception, match="Unexpected Error"): get_mapping_fields_command(client=None, args={}) def test_doppel_get_alert_command(client, mocker): # Mock API response mocker.patch.object(client, "get_alert", return_value={"id": "TET-1953443", "status": "Open", "name": "Test Alert"}) args = {"id": "TET-1953443"} result = doppel_get_alert_command(client, args) assert isinstance(result, CommandResults), f"Expected CommandResults but got {type(result)}" assert result.outputs_prefix == "Doppel.Alert" assert result.outputs_key_field == "id" assert result.outputs.get("id") == "TET-1953443" assert "Alert Summary" in result.readable_output def test_doppel_get_alert_command_with_invalid_params(client): args = {"id": "TET-1953443", "entity": "http://test-doppel.com"} with pytest.raises(ValueError): doppel_get_alert_command(client, args) def test_doppel_get_alert_command_with_missing_params(client): args = {} with pytest.raises(ValueError): doppel_get_alert_command(client, args) def mock_no_alert_found(*args, **kwargs): raise DemistoException("No alert found with the given parameters.") def test_doppel_get_alert_command_with_no_alert_found(client, mocker): mocker.patch.object(client, "get_alert", side_effect=mock_no_alert_found) args = {"id": "NON_EXISTENT_ID"} with pytest.raises(Exception): doppel_get_alert_command(client, args) def test_doppel_update_alert_command(mocker): """Test doppel_update_alert_command function with an inline mock client.""" # Mocking the Client instance mock_client = MagicMock() mock_client.update_alert.return_value = {"id": "123", "queue_state": "archived", "entity_state": "closed"} # Sample arguments args = {"alert_id": "123", "queue_state": "archived", "entity_state": "closed", "comment": "Resolved"} # Run the function result = doppel_update_alert_command(mock_client, args) # Assertions assert result.outputs_prefix == "Doppel.UpdatedAlert", "Incorrect outputs prefix" assert result.outputs_key_field == "id", "Incorrect key field" assert result.outputs == {"id": "123", "queue_state": "archived", "entity_state": "closed"}, "Unexpected output" def test_doppel_update_alert_command_negative_cases(): """Test doppel_update_alert_command for various negative scenarios.""" mock_client = MagicMock() # Case 1: Both alert_id and entity are provided args_conflict = {"alert_id": "123", "entity": "some_entity", "queue_state": "archived"} with pytest.raises(ValueError, match="Only one of 'alert_id' or 'entity' can be specified."): doppel_update_alert_command(mock_client, args_conflict) # Case 2: No update fields provided args_missing_fields = {"alert_id": "123"} with pytest.raises(ValueError, match="At least one of 'queue_state', 'entity_state', or 'comment' must be provided."): doppel_update_alert_command(mock_client, args_missing_fields) # Case 3: API Failure (Simulated by raising an exception in mock) mock_client.update_alert.side_effect = Exception("API error: Alert not found") args_api_error = {"alert_id": "999", "queue_state": "archived"} with pytest.raises(Exception, match="Failed to update the alert with the given parameters :- API error: Alert not found"): doppel_update_alert_command(mock_client, args_api_error) def test_doppel_update_alert_command_with_entity(client, mocker): # Prepare the mock response for the _http_request function mocker.patch.object(client, "_http_request", side_effect=mock_http_request) # Sample arguments to simulate the command input, using 'entity' instead of 'alert_id' args = { "alert_id": "", # Empty alert_id to test entity usage "queue_state": "doppel_review", "entity_state": "active", "entity": "http://test-doppel.com", # Provide an entity for testing "comment": "Test update comment", } mock_response = util_load_json("test_data/get-alert.json") # Set up the mock return value client.update_alert.return_value = mock_response # Call the command function result = doppel_update_alert_command(client, args) # Assert that the result's human-readable output is generated correctly assert "Alert Summary" in result.readable_output # Check if the title exists assert isinstance(result, CommandResults) # Ensure the result is a CommandResults object assert result.outputs_prefix == "Doppel.UpdatedAlert" # Ensure the outputs prefix is correct assert result.outputs_key_field == "id" # Ensure the key field is correct assert result.outputs == mock_response # Ensure the correct output is returned def test_doppel_get_alerts_command(client, mocker): mock_data = util_load_json("test_data/get-all-alerts.json") mocker.patch.object(client, "get_alerts", return_value=mock_data) args = { "search_key": "test-key", "queue_state": "open", "product": "domains", "created_before": "2025-01-01T00:00:00Z", "created_after": "2025-01-01T00:00:00Z", "sort_type": "created", "sort_order": "asc", "page": 1, "tags": "tag1,tag2", } result = doppel_get_alerts_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.GetAlerts" assert result.outputs_key_field == "id" assert result.outputs["alerts"][0]["id"] == "TET-1953443" assert "Alert Summary" in result.readable_output def test_doppel_get_alerts_command_no_results(client, mocker): """Test doppel_get_alerts_command when no alerts are found.""" # Mock the API response to return an empty list mocker.patch.object(client, "get_alerts", return_value={"alerts": []}) args = { "search_key": "non-existent-key", "queue_state": "closed", "product": "unknown", "created_before": "2025-01-01T00:00:00Z", "created_after": "2025-01-01T00:00:00Z", "sort_type": "created", "sort_order": "asc", "page": 1, "tags": "invalid-tag", } result = doppel_get_alerts_command(client, args) assert isinstance(result, CommandResults) assert result.outputs == {"alerts": []} # Expecting an empty result assert "No alerts were found" not in result.readable_output # Should not raise an error, just be empty def test_doppel_get_alerts_command_api_error(client, mocker): """Test doppel_get_alerts_command when API raises an exception.""" # Mock the API call to raise an exception mocker.patch.object(client, "get_alerts", side_effect=Exception("API failure")) args = { "search_key": "test-key", "queue_state": "open", "product": "domains", "created_before": "2025-01-01T00:00:00Z", "created_after": "2025-01-01T00:00:00Z", "sort_type": "created", "sort_order": "asc", "page": 1, "tags": "tag1,tag2", } with pytest.raises(Exception, match="No alerts were found with the given parameters :- API failure."): doppel_get_alerts_command(client, args) def test_doppel_create_alert_command(client, mocker): test_response = util_load_json("test_data/create-alert.json") client.create_alert.return_value = test_response args = { "entity": "test-doppel.com" # Ensure 'entity' is included in the arguments } result = doppel_create_alert_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.CreatedAlert" assert result.outputs_key_field == "id" assert result.outputs == test_response # Check if the result matches the mocked response assert "Alert Summary" in result.readable_output def test_doppel_create_alert_command_missing_entity(client): """Test case when 'entity' is missing in the arguments.""" args = {} # Missing 'entity' with pytest.raises(ValueError, match="Entity must be specified to create an alert."): doppel_create_alert_command(client, args) def test_doppel_create_alert_command_failure(mocker): """Test doppel_create_alert_command when alert creation fails.""" # Mock client mock_client = MagicMock() # Simulate API failure mock_client.create_alert.side_effect = Exception("API call failed") # Define arguments test_args = {"entity": "test_entity"} # Verify exception is raised with pytest.raises(Exception, match="Failed to create the alert with the given parameters:- API call failed"): doppel_create_alert_command(client=mock_client, args=test_args) def test_doppel_get_alerts_no_results(mocker): """Test when no alerts are found (empty response).""" mock_client = MagicMock() mock_client.get_alerts.return_value = {"alerts": []} test_args = {"queue_state": "resolved"} result = doppel_get_alerts_command(mock_client, test_args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.GetAlerts" assert result.outputs == {"alerts": []} assert "No alerts were found" not in result.readable_output def test_doppel_get_alerts_missing_params(mocker): """Test when query parameters are missing.""" mock_client = MagicMock() mock_client.get_alerts.return_value = {"alerts": [{"id": "125", "name": "Alert"}]} test_args = {} # No parameters provided result = doppel_get_alerts_command(mock_client, test_args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.GetAlerts" assert len(result.outputs) == 1 assert result.outputs["alerts"][0]["id"] == "125" def test_doppel_get_alerts_optional_params(mocker): """Test handling of optional parameters like tags and pagination.""" mock_client = MagicMock() mock_client.get_alerts.return_value = {"alerts": [{"id": "126", "name": "Optional Param Test"}]} test_args = {"tags": "phishing,low", "page": "2"} result = doppel_get_alerts_command(mock_client, test_args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.GetAlerts" assert len(result.outputs) == 1 assert result.outputs["alerts"][0]["name"] == "Optional Param Test" def test_doppel_create_abuse_alert_command(client, mocker): test_response = util_load_json("test_data/create-abuse-alert.json") client.create_abuse_alert.return_value = test_response args = {"entity": "test-doppel.com"} result = doppel_create_abuse_alert_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.AbuseAlert" assert result.outputs_key_field == "id" expected_output = util_load_json("test_data/create-abuse-alert.json") assert result.outputs == expected_output assert "Alert Summary" in result.readable_output def test_doppel_create_abuse_alert_command_missing_entity(client): args = {} with pytest.raises(ValueError, match="Entity must be specified to create an abuse alert."): doppel_create_abuse_alert_command(client, args) def test_doppel_create_abuse_alert_command_failure(mocker): """Test doppel_create_abuse_alert_command when abuse alert creation fails.""" # Mock client mock_client = MagicMock() # Simulate API failure mock_client.create_abuse_alert.side_effect = Exception("API call failed") # Define arguments test_args = {"entity": "test_entity"} # Verify exception is raised with pytest.raises(Exception, match="Failed to create the abuse alert with the given parameters:- API call failed"): doppel_create_abuse_alert_command(client=mock_client, args=test_args) def test_get_modified_remote_data_command(mocker): """ Test that `get_modified_remote_data_command` returns modified incident IDs. """ mock_client = MagicMock() mock_alerts = [ {"id": "alert-001", "name": "Alert 1"}, {"id": "alert-002", "name": "Alert 2"}, ] mock_client.get_alerts.return_value = {"alerts": mock_alerts} args = {"lastUpdate": "2025-02-24T14:30:00Z"} mocker.patch.object(demisto, "debug") result = get_modified_remote_data_command(mock_client, args) assert result.modified_incident_ids == ["alert-001", "alert-002"] def test_doppel_update_alert_both_alert_id_and_entity(mocker): """Test failure when both alert_id and entity are provided.""" mock_client = MagicMock() test_args = {"alert_id": "123", "entity": "TestEntity", "queue_state": "open"} with pytest.raises(ValueError, match="Only one of 'alert_id' or 'entity' can be specified."): doppel_update_alert_command(mock_client, test_args) def test_doppel_update_alert_no_update_fields(mocker): """Test failure when no update fields are provided.""" mock_client = MagicMock() test_args = {"alert_id": "123"} with pytest.raises(ValueError, match="At least one of 'queue_state', 'entity_state', or 'comment' must be provided."): doppel_update_alert_command(mock_client, test_args) def test_doppel_update_alert_api_failure(mocker): """Test API failure handling when an exception is raised.""" mock_client = MagicMock() mock_client.update_alert.side_effect = Exception("API error") test_args = {"alert_id": "123", "queue_state": "open"} with pytest.raises(Exception, match="Failed to update the alert with the given parameters"): doppel_update_alert_command(mock_client, test_args) def test_doppel_update_alert_partial_update(mocker): """Test updating an alert with only one field (entity_state).""" mock_client = MagicMock() mock_client.update_alert.return_value = {"id": "124", "entity_state": "investigating"} test_args = {"alert_id": "124", "entity_state": "investigating"} result = doppel_update_alert_command(mock_client, test_args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.UpdatedAlert" assert result.outputs["id"] == "124" assert result.outputs["entity_state"] == "investigating" def test_doppel_update_alert_only_entity(mocker): """Test updating an alert using 'entity' instead of 'alert_id'.""" mock_client = MagicMock() mock_client.update_alert.return_value = {"id": "125", "queue_state": "open", "entity": "TestEntity"} test_args = {"entity": "TestEntity", "queue_state": "open"} result = doppel_update_alert_command(mock_client, test_args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.UpdatedAlert" assert result.outputs["entity"] == "TestEntity" assert result.outputs["queue_state"] == "open" def test_doppel_update_alert_only_queue_state(mocker): """Test updating an alert with only queue_state provided.""" mock_client = MagicMock() mock_client.update_alert.return_value = {"id": "126", "queue_state": "archived"} test_args = {"alert_id": "126", "queue_state": "archived"} result = doppel_update_alert_command(mock_client, test_args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Doppel.UpdatedAlert" assert result.outputs["queue_state"] == "archived" def test_format_datetime(): """Test format_datetime with various datetime formats.""" # Test valid ISO 8601 format assert format_datetime("2025-02-27T14:30:00") == "2025-02-27T14:30:00" # Test ISO 8601 with 'Z' assert format_datetime("2025-02-27T14:30:00Z") == "2025-02-27T14:30:00+00:00" # Test empty input assert format_datetime("") is None # Test None input assert format_datetime(None) is None # Test invalid format with pytest.raises(ValueError): format_datetime("invalid-date") def test_paginated_call_to_get_alerts(): """Test the _paginated_call_to_get_alerts function.""" # Mock client and response mock_client = MagicMock() mock_client.get_alerts.return_value = {"alerts": [{"id": "alert1"}, {"id": "alert2"}]} # Define test inputs page = 1 last_fetch_datetime = datetime(2025, 2, 27, 14, 30, 0) # Call function result = _paginated_call_to_get_alerts(mock_client, page, last_fetch_datetime) # Assertions expected_query_params = { "created_after": last_fetch_datetime.strftime(DOPPEL_API_DATE_FORMAT), "sort_type": "date_sourced", "sort_order": "asc", "page": page, "page_size": 200, } mock_client.get_alerts.assert_called_once_with(params=expected_query_params) # Ensure correct API call assert isinstance(result, list) # Should return a list assert len(result) == 2 # Should return 2 alerts assert result[0]["id"] == "alert1" # Validate alert content assert result[1]["id"] == "alert2" # Test case where no alerts are returned mock_client.get_alerts.return_value = {} result = _paginated_call_to_get_alerts(mock_client, page, last_fetch_datetime) assert result is None # Should return None if no alerts key exists def test_get_last_fetch_datetime(): """Test _get_last_fetch_datetime with different inputs.""" # Test case: Valid last_run timestamp last_run = "2025-02-24T14:30:00Z" expected_datetime = datetime.strptime(last_run, "%Y-%m-%dT%H:%M:%SZ") assert _get_last_fetch_datetime(last_run) == expected_datetime # Test case: No last_run, using first_fetch with "3 days" result = _get_last_fetch_datetime(None) assert isinstance(result, datetime) # Ensure result is a datetime object # Test case: Invalid last_run format should raise ValueError with pytest.raises(ValueError): _get_last_fetch_datetime("invalid-date") def test_get_mirroring_fields(): """Test _get_mirroring_fields function.""" # Mocking expected return values demisto_params = {"mirror_direction": "Both"} demisto_instance = "Test_Integration" # Setting mock values manually demisto.params = lambda: demisto_params demisto.integrationInstance = lambda: demisto_instance expected_result = { "mirror_direction": MIRROR_DIRECTION.get("Both"), "mirror_instance": "Test_Integration", "incident_type": "Doppel_Incident", } assert _get_mirroring_fields() == expected_result def test_get_remote_updated_incident_data_with_entry(): """Test _get_remote_updated_incident_data_with_entry with a mock client.""" # Mock client mock_client = MagicMock() # Test data doppel_alert_id = "12345" last_update_str = "2025-02-24T14:30:00.120000Z" # ISO format # Mock API response mock_client.get_alert.return_value = { "id": doppel_alert_id, "audit_logs": [ {"timestamp": "2024-11-27T06:51:50.357664", "type": "alert_create"}, {"timestamp": "2024-11-27T06:51:50.357664", "type": "alert_create"}, ], } # Call function updated_alert, entries = _get_remote_updated_incident_data_with_entry(mock_client, doppel_alert_id, last_update_str) # Assertions assert updated_alert or updated_alert is None, "Updated alert should be either valid or None" def test_get_remote_updated_incident_data_never_synced_timestamp(): """ Given: - A lastUpdate timestamp of a never-synced incident ("0001-01-01T00:00:00Z", no microseconds). When: - Running _get_remote_updated_incident_data_with_entry. Then: - The unparseable timestamp does not raise, and the updated alert is still returned so the first incoming mirror sync completes. """ mock_client = MagicMock() mock_client.get_alert.return_value = { "id": "12345", "queue_state": "actioned", "audit_logs": [{"timestamp": "2024-11-27T06:51:50.357664", "type": "alert_create"}], } updated_alert, entries = _get_remote_updated_incident_data_with_entry(mock_client, "12345", "0001-01-01T00:00:00Z") assert updated_alert is not None assert updated_alert["queue_state"] == "actioned" assert len(entries) == 1 def test_get_remote_updated_incident_data_no_audit_logs(): """ Given: - An updated alert whose payload has no audit logs. When: - Running _get_remote_updated_incident_data_with_entry. Then: - The alert field updates are still returned (not discarded), with no note entries. """ mock_client = MagicMock() mock_client.get_alert.return_value = { "id": "12345", "queue_state": "actioned", } updated_alert, entries = _get_remote_updated_incident_data_with_entry(mock_client, "12345", "2025-02-24T14:30:00.120000Z") assert updated_alert is not None assert updated_alert["queue_state"] == "actioned" assert entries == [] def test_get_remote_updated_incident_data_empty_audit_logs(): """ Given: - An updated alert whose audit_logs list is empty. When: - Running _get_remote_updated_incident_data_with_entry. Then: - No exception is raised and the alert field updates are still returned. """ mock_client = MagicMock() mock_client.get_alert.return_value = { "id": "12345", "queue_state": "actioned", "audit_logs": [], } updated_alert, entries = _get_remote_updated_incident_data_with_entry(mock_client, "12345", "2025-02-24T14:30:00.120000Z") assert updated_alert is not None assert updated_alert["queue_state"] == "actioned" assert entries == [] def test_get_modified_remote_data_command_paginates(mocker): """ Given: - More modified alerts than fit in a single API page. When: - Running get_modified_remote_data_command. Then: - All pages are drained and every modified alert ID is returned exactly once. """ mock_client = MagicMock() first_page = [{"id": f"alert-{i:03d}"} for i in range(200)] second_page = [{"id": f"alert-{i:03d}"} for i in range(200, 250)] mock_client.get_alerts.side_effect = [{"alerts": first_page}, {"alerts": second_page}] mocker.patch.object(demisto, "debug") result = get_modified_remote_data_command(mock_client, {"lastUpdate": "2025-02-24T14:30:00Z"}) assert len(result.modified_incident_ids) == 250 assert result.modified_incident_ids[0] == "alert-000" assert result.modified_incident_ids[-1] == "alert-249" assert mock_client.get_alerts.call_count == 2 first_call_params = mock_client.get_alerts.call_args_list[0][1]["params"] second_call_params = mock_client.get_alerts.call_args_list[1][1]["params"] assert first_call_params["page"] == 0 assert second_call_params["page"] == 1 assert first_call_params["page_size"] == 200 def test_client_initialization_with_proxy(mocker): """Test Client initialization with proxy enabled.""" base_url = "https://api.doppel.com/v1" api_key = "test-api-key" # Mock BaseClient.__init__ to verify proxy is passed correctly mock_base_init = mocker.patch("Doppel.BaseClient.__init__", return_value=None) # Create client with proxy enabled Client(base_url=base_url, api_key=api_key, proxy=True, verify=True) # Verify BaseClient was initialized with proxy=True mock_base_init.assert_called_once() call_kwargs = mock_base_init.call_args[1] assert call_kwargs["proxy"] assert call_kwargs["verify"] def test_client_initialization_without_proxy(mocker): """Test Client initialization with proxy disabled.""" base_url = "https://api.doppel.com/v1" api_key = "test-api-key" # Mock BaseClient.__init__ mock_base_init = mocker.patch("Doppel.BaseClient.__init__", return_value=None) # Create client with proxy disabled Client(base_url=base_url, api_key=api_key, proxy=False, verify=True) # Verify BaseClient was initialized with proxy=False mock_base_init.assert_called_once() call_kwargs = mock_base_init.call_args[1] assert call_kwargs["proxy"] is False def test_client_initialization_proxy_default_none(mocker): """Test Client initialization with proxy parameter not specified (defaults to None).""" base_url = "https://api.doppel.com/v1" api_key = "test-api-key" # Mock BaseClient.__init__ mock_base_init = mocker.patch("Doppel.BaseClient.__init__", return_value=None) # Create client without specifying proxy Client(base_url=base_url, api_key=api_key, verify=True) # Verify BaseClient was initialized with proxy=None mock_base_init.assert_called_once() call_kwargs = mock_base_init.call_args[1] assert call_kwargs["proxy"] is None def test_main_function_with_proxy_enabled(mocker): """Test main function when proxy is enabled in params.""" # Mock demisto functions mocker.patch.object( demisto, "params", return_value={ "url": "https://api.doppel.com", "credentials": {"password": "test-api-key"}, "proxy": True, "insecure": False, }, ) mocker.patch.object(demisto, "command", return_value="test-module") mocker.patch.object(demisto, "args", return_value={}) # Mock Client initialization mock_client_init = mocker.patch("Doppel.Client") # Mock test_module to return 'ok' mocker.patch("Doppel.test_module", return_value="ok") mocker.patch("Doppel.return_results") # Call main from Doppel import main main() # Verify Client was called with proxy=True mock_client_init.assert_called_once() call_kwargs = mock_client_init.call_args[1] assert call_kwargs["proxy"] is True assert call_kwargs["verify"] is True def test_main_function_with_proxy_disabled(mocker): """Test main function when proxy is disabled in params.""" # Mock demisto functions mocker.patch.object( demisto, "params", return_value={ "url": "https://api.doppel.com", "credentials": {"password": "test-api-key"}, "proxy": False, "insecure": False, }, ) mocker.patch.object(demisto, "command", return_value="test-module") mocker.patch.object(demisto, "args", return_value={}) # Mock Client initialization mock_client_init = mocker.patch("Doppel.Client") # Mock test_module to return 'ok' mocker.patch("Doppel.test_module", return_value="ok") mocker.patch("Doppel.return_results") # Call main from Doppel import main main() # Verify Client was called with proxy=False mock_client_init.assert_called_once() call_kwargs = mock_client_init.call_args[1] assert call_kwargs["proxy"] is False def test_parse_fetch_timeout(mocker): """fetch_timeout is parsed to a float; blank/invalid/missing yields no limit (None).""" mocker.patch.object(demisto, "params", return_value={"fetch_timeout": "30"}) assert _parse_fetch_timeout() == 30.0 mocker.patch.object(demisto, "params", return_value={"fetch_timeout": ""}) assert _parse_fetch_timeout() is None mocker.patch.object(demisto, "params", return_value={"fetch_timeout": None}) assert _parse_fetch_timeout() is None mocker.patch.object(demisto, "params", return_value={"fetch_timeout": "abc"}) assert _parse_fetch_timeout() is None mocker.patch.object(demisto, "params", return_value={}) assert _parse_fetch_timeout() is None def test_parse_max_fetch(mocker): """max_fetch is parsed to a positive int; blank/invalid/non-positive falls back to the default.""" mocker.patch.object(demisto, "params", return_value={"max_fetch": "25"}) assert _parse_max_fetch() == 25 mocker.patch.object(demisto, "params", return_value={"max_fetch": ""}) assert _parse_max_fetch() == 10 mocker.patch.object(demisto, "params", return_value={"max_fetch": "0"}) assert _parse_max_fetch() == 10 mocker.patch.object(demisto, "params", return_value={"max_fetch": "-5"}) assert _parse_max_fetch() == 10 mocker.patch.object(demisto, "params", return_value={}) assert _parse_max_fetch() == 10 def test_incident_alert_id(): """The Doppel alert id is read from dbotMirrorId first, then from rawJSON, else empty.""" assert _incident_alert_id({"dbotMirrorId": "TET-1"}) == "TET-1" assert _incident_alert_id({"rawJSON": json.dumps({"id": "TET-2"})}) == "TET-2" assert _incident_alert_id({"rawJSON": "not-json"}) == "" assert _incident_alert_id({}) == "" def test_alert_to_incident(): """An alert becomes an incident named by its external id, with dbotMirrorId, severity, and merged mirroring fields.""" alert = {"id": "TET-1953443", "created_at": "2025-01-27T07:55:10.063742", "severity": "high"} incident = _alert_to_incident(alert, {"mirror_direction": "In"}) assert incident["name"] == "Doppel Alert TET-1953443" assert incident["type"] == "Doppel Alert" assert incident["dbotMirrorId"] == "TET-1953443" assert incident["occurred"] == "2025-01-27T07:55:10Z" assert incident["severity"] == 3 raw = json.loads(incident["rawJSON"]) assert raw["mirror_direction"] == "In" def test_xsoar_severity(): """Doppel severities map to XSOAR numeric severities; unknown/blank values default to 0 (Unknown).""" assert _xsoar_severity({"severity": "low"}) == 1 assert _xsoar_severity({"severity": "Medium"}) == 2 assert _xsoar_severity({"severity": "HIGH"}) == 3 assert _xsoar_severity({"severity": "critical"}) == 4 assert _xsoar_severity({"severity": "bogus"}) == 0 assert _xsoar_severity({}) == 0 def _fetch_demisto_mocks(mocker, params, last_run): mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(demisto, "getLastRun", return_value=last_run) set_last_run = mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "info") mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "integrationInstance", return_value="inst") return set_last_run def test_fetch_incidents_dedupes_within_run(mocker): """A duplicated alert id inside a single page creates only one incident.""" _fetch_demisto_mocks( mocker, params={"max_fetch": 50, "fetch_timeout": "30", "mirror_direction": "None"}, last_run={}, ) page = [ {"id": "TET-1", "created_at": "2025-01-27T07:55:10.063742"}, {"id": "TET-1", "created_at": "2025-01-27T07:55:10.063742"}, # duplicate id in same page {"id": "TET-2", "created_at": "2025-01-27T07:55:11.063742"}, ] mocker.patch("Doppel._paginated_call_to_get_alerts", side_effect=[page, []]) fetch_incidents_command(client=MagicMock(), args={}) created = demisto.incidents.call_args[0][0] assert sorted(i["name"] for i in created) == ["Doppel Alert TET-1", "Doppel Alert TET-2"] def test_fetch_incidents_skips_recently_seen_ids(mocker): """Ids persisted from the previous run's boundary second are not re-created on the inclusive re-pull.""" _fetch_demisto_mocks( mocker, params={"max_fetch": 50, "fetch_timeout": "30", "mirror_direction": "None"}, last_run={"last_run": "2025-01-27T07:55:10Z", "incidents_queue": [], "recently_seen_ids": ["TET-1"]}, ) page = [ {"id": "TET-1", "created_at": "2025-01-27T07:55:10.063742"}, # seen on the prior run {"id": "TET-2", "created_at": "2025-01-27T07:55:12.063742"}, # genuinely new ] mocker.patch("Doppel._paginated_call_to_get_alerts", side_effect=[page, []]) fetch_incidents_command(client=MagicMock(), args={}) created = demisto.incidents.call_args[0][0] assert [i["name"] for i in created] == ["Doppel Alert TET-2"] def test_fetch_incidents_persists_boundary_ids(mocker): """The cursor advances to the newest alert second and only that second's ids are persisted.""" set_last_run = _fetch_demisto_mocks( mocker, params={"max_fetch": 50, "fetch_timeout": "30", "mirror_direction": "None"}, last_run={}, ) page = [ {"id": "TET-1", "created_at": "2025-01-27T07:55:10.063742"}, {"id": "TET-2", "created_at": "2025-01-27T07:55:12.063742"}, # newest second ] mocker.patch("Doppel._paginated_call_to_get_alerts", side_effect=[page, []]) fetch_incidents_command(client=MagicMock(), args={}) created = demisto.incidents.call_args[0][0] assert all(i["dbotMirrorId"] for i in created) last_run_data = set_last_run.call_args[0][0] assert last_run_data["last_run"] == "2025-01-27T07:55:12Z" assert last_run_data["recently_seen_ids"] == ["TET-2"]