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.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * import json import traceback from datetime import datetime, UTC """Doppel for Cortex XSOAR (aka Demisto) This integration contains features to mirror the alerts from Doppel to create incidents in XSOAR and the commands to perform different updates on the alerts """ import urllib3 from typing import Any, Callable # noqa: UP035 # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ XSOAR_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" DOPPEL_API_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" DOPPEL_PAYLOAD_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" MIRROR_DIRECTION = { "None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both", } DOPPEL_ALERT = "Doppel Alert" DOPPEL_INCIDENT = "Doppel Incident" DEFAULT_RETRY_TOTAL = 3 DEFAULT_RETRY_BACKOFF_FACTOR = 2 DEFAULT_RETRY_STATUS_LIST = [429, 500, 502, 503, 504] # Doppel's get-alerts API caps a page at 200 results; request the max to drain backlogs in fewer calls. DOPPEL_MAX_PAGE_SIZE = 200 # Fallback when max_fetch is blank/invalid. DEFAULT_MAX_FETCH = 10 # Hard ceiling on pages pulled in a single fetch run, so a misbehaving API can never spin forever. MAX_FETCH_PAGES_PER_RUN = 1000 # Map Doppel alert severities to XSOAR incident severities (0 = Unknown). SEVERITY_MAP = {"low": 1, "medium": 2, "high": 3, "critical": 4} """ CLIENT CLASS """ class Client(BaseClient): """Client class to interact with the service API This Client implements API calls, and does not contain any XSOAR logic. Should only do requests and return data. It inherits from BaseClient defined in CommonServer Python. Most calls use _http_request() that handles proxy, SSL verification, etc. For this implementation, no special attributes defined """ def __init__( self, base_url, api_key, user_api_key=None, organization_code=None, verify=None, proxy=None, retry_total=DEFAULT_RETRY_TOTAL, retry_backoff_factor=DEFAULT_RETRY_BACKOFF_FACTOR, retry_status_list=DEFAULT_RETRY_STATUS_LIST, ): super().__init__(base_url, verify=verify, proxy=proxy) self._headers = {"accept": "application/json", "x-api-key": api_key} if user_api_key: self._headers["x-user-api-key"] = user_api_key if organization_code: self._headers["x-organization-code"] = organization_code # Store retry configuration on the client and leverage BaseClient._http_request parameters self._retries = retry_total self._backoff_factor = retry_backoff_factor self._status_list_to_retry = retry_status_list demisto.debug( f"Initialized HTTP client using BaseClient._http_request retry params: total={retry_total}, " f"backoff_factor={retry_backoff_factor}, status_list={retry_status_list}" ) def get_alert(self, id: str, entity: str) -> dict[str, Any]: """Return the alert's details when provided the Alert ID or Entity as input :type id: ``str`` :param id: Alert id for which we need to fetch details :type entity: ``str`` :param entity: Alert id for which we need to fetch details :return: dict as with alert's details :rtype: ``dict`` """ params: dict = {} if id: params["id"] = id if entity: params["entity"] = entity response_content = self._http_request( method="GET", url_suffix="alert", params=params, retries=self._retries, backoff_factor=self._backoff_factor, status_list_to_retry=self._status_list_to_retry, ) return response_content def update_alert( self, queue_state: str, entity_state: str, alert_id: str | None = None, entity: str | None = None, comment: str | None = None, ) -> dict[str, Any]: """ Updates an existing alert using either the alert ID or the entity. :param queue_state: The queue state to update to. :param entity_state: The entity state to update to. :param alert_id: The alert ID (optional). :param entity: The entity (optional). :param comment: The comment (optional). :return: JSON response containing the updated alert. """ if alert_id and entity: raise ValueError("Only one of 'alert_id' or 'entity' can be specified, not both.") if not alert_id and not entity: raise ValueError("Either 'alert_id' or 'entity' must be specified.") api_name = "alert" api_url = f"{self._base_url}/{api_name}" params = {} if alert_id: params["id"] = alert_id elif entity: params["entity"] = entity payload = {"queue_state": queue_state, "entity_state": entity_state, "comment": comment} response_content = self._http_request( method="PUT", full_url=api_url, params=params, json_data=payload, retries=self._retries, backoff_factor=self._backoff_factor, status_list_to_retry=self._status_list_to_retry, ) return response_content def get_alerts(self, params: dict[str, Any]) -> dict[str, Any]: """ Fetches multiple alerts based on query parameters. :param params: A dictionary of query parameters to apply to the request. :return: A list of dictionaries containing alert details. """ api_name = "alerts" api_url = f"{self._base_url}/{api_name}" # Filter out None values filtered_params = {k: v for k, v in params.items() if v is not None} demisto.debug(f"API Request Params: {filtered_params}") response_content = self._http_request( method="GET", full_url=api_url, params=filtered_params, retries=self._retries, backoff_factor=self._backoff_factor, status_list_to_retry=self._status_list_to_retry, ) return response_content def create_alert(self, entity: str) -> dict[str, Any]: api_name = "alert" api_url = f"{self._base_url}/{api_name}" response_content = self._http_request( method="POST", full_url=api_url, json_data={"entity": entity}, retries=self._retries, backoff_factor=self._backoff_factor, status_list_to_retry=self._status_list_to_retry, ) return response_content def create_abuse_alert(self, entity: str) -> dict[str, Any]: api_name = "alert/abuse" api_url = f"{self._base_url}/{api_name}" response_content = self._http_request( method="POST", full_url=api_url, json_data={"entity": entity}, retries=self._retries, backoff_factor=self._backoff_factor, status_list_to_retry=self._status_list_to_retry, ) return response_content """ HELPER FUNCTIONS """ def _normalize_entity_content_for_grid(entity_content: Any) -> list[dict[str, Any]]: """ Convert Doppel entity_content into a list of row dicts for the grid incident field. Domains alerts nest fields under ``root_domain``; the XSOAR grid field expects a list of flat objects whose keys match the grid columns (domain, registrar, ip_address, ...). """ if not entity_content: return [] if isinstance(entity_content, list): return [row for row in entity_content if isinstance(row, dict)] if not isinstance(entity_content, dict): return [] root_domain = entity_content.get("root_domain") if isinstance(root_domain, dict): return [root_domain] # Other product shapes: unwrap a single nested dict when present nested_dicts = [value for value in entity_content.values() if isinstance(value, dict)] if len(nested_dicts) == 1: return [nested_dicts[0]] # Already a flat row (no nested objects): use it as-is so no data is dropped if not nested_dicts: return [entity_content] return [] def _get_remote_updated_incident_data_with_entry(client: Client, doppel_alert_id: str, last_update_str: str): """ Retrieves updated incident data from the remote system based on the given alert ID and last update timestamp. Args: client (Client): An instance of the Client class used to interact with the remote Doppel API. doppel_alert_id (str): The unique identifier of the alert in the remote system. last_update_str (str): A string representing the last update timestamp in ISO 8601 format (e.g., "2025-01-19T08:44:52Z"). Returns: dict[str, Any]: A dictionary containing the updated incident details, including entries related to the alert. """ # A never-synced incident carries "0001-01-01T00:00:00Z" (no microseconds), which the # previous strict strptime call could not parse, so the first sync never completed. # The timestamp is only informational here (the server already filtered this incident as # modified via get-modified-remote-data), so a parse failure must not block the sync. try: last_update = arg_to_datetime(last_update_str, required=False) except ValueError: last_update = None if not last_update: demisto.debug(f"Doppel - Could not parse lastUpdate timestamp {last_update_str!r}; syncing anyway.") demisto.debug(f"Getting Remote Data for {doppel_alert_id} which was last updated on: {last_update}") updated_doppel_alert = client.get_alert(id=doppel_alert_id, entity="") demisto.debug(f"Received alert data for {doppel_alert_id}") if not updated_doppel_alert: demisto.debug(f"Doppel - No alert data returned for {doppel_alert_id}.") return None, [] updated_doppel_alert["id"] = doppel_alert_id # Shape entity_content for the grid incident field (mapper expects a list of row dicts). if "entity_content" in updated_doppel_alert: updated_doppel_alert["entity_content"] = _normalize_entity_content_for_grid(updated_doppel_alert.get("entity_content")) # Attach the most recent audit-log event as a War Room note when available, but never # let a missing/empty audit trail discard the field updates themselves. entries: list = [] audit_logs = updated_doppel_alert.get("audit_logs") demisto.debug(f"The alert contains {len(audit_logs) if isinstance(audit_logs, list) else 0} audit logs") if isinstance(audit_logs, list) and audit_logs and all(isinstance(log, dict) for log in audit_logs): # ISO-8601 timestamps sort lexicographically, so string comparison is safe and avoids # strptime failures on entries without microseconds. most_recent_audit_log = max(audit_logs, key=lambda audit_log: str(audit_log.get("timestamp") or "")) demisto.debug(f"Most recent audit log is {most_recent_audit_log}") entries = [{"Type": EntryType.NOTE, "Contents": most_recent_audit_log, "ContentsFormat": EntryFormat.JSON, "Note": True}] demisto.debug(f"Successfully returning the updated alert and entries: {updated_doppel_alert, entries}") return updated_doppel_alert, entries def _get_mirroring_fields(): """ Get tickets mirroring. """ mirror_direction: str = demisto.params().get("mirror_direction", "None") return { "mirror_direction": MIRROR_DIRECTION.get(mirror_direction), "mirror_instance": demisto.integrationInstance(), "incident_type": "Doppel_Incident", } def _get_last_fetch_datetime(last_run): # Fetch the last run (time of the last fetch) last_fetch_datetime: datetime = datetime.now() if last_run: last_fetch_datetime = datetime.strptime(last_run, "%Y-%m-%dT%H:%M:%SZ") demisto.debug(f"Alerts were fetched last on: {last_fetch_datetime}") else: # If no last run is found first_fetch_time = demisto.params().get("first_fetch", "3 days").strip() try: last_fetch_datetime = arg_to_datetime(first_fetch_time, required=False) or datetime.now() except ValueError: last_fetch_datetime = datetime.now() demisto.debug(f"This is the first time we are fetching the incidents. This time fetching it from: {last_fetch_datetime}") return last_fetch_datetime def _paginated_call_to_get_alerts(client, page, last_fetch_datetime): """ Set the query parameters """ last_fetch_str: str = last_fetch_datetime.strftime(DOPPEL_API_DATE_FORMAT) query_params = { "created_after": last_fetch_str, # Fetch alerts after the last_fetch, "sort_type": "date_sourced", "sort_order": "asc", "page": page, "page_size": DOPPEL_MAX_PAGE_SIZE, # Pull large pages to drain backlogs in fewer requests (API max is 200) } get_alerts_response = client.get_alerts(params=query_params) alerts = get_alerts_response.get("alerts", None) return alerts """ COMMAND FUNCTIONS """ def test_module(client: Client) -> str: """Tests API connectivity and authentication' Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful.password Raises exceptions if something goes wrong. :type client: ``Client`` :param Client: client to use :return: 'ok' if test passed, anything else will fail the test. :rtype: ``str`` """ try: # Using the same dates so that we do not fetch any data for testing, # but still get the response as 200 current_datetime_str = datetime.now().strftime(DOPPEL_API_DATE_FORMAT) query_params = {"created_before": current_datetime_str, "created_after": current_datetime_str} # Call the client's `get_alerts` method to test the connection client.get_alerts(params=query_params) message: str = "ok" except DemistoException as e: if "Forbidden" in str(e) or "Authorization" in str(e): message = "Authorization Error: make sure API Key is correctly set" else: raise e return message def doppel_get_alert_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Comand to get a specific alert in the Doppel client using the provided arguments. :param client: Client instance to interact with the API. :param args: Command arguments containing the query parameters as key-value pairs. :return: CommandResults object including alert details. """ id: str = args.get("id", "") entity: str = args.get("entity", "") if not id and not entity: raise ValueError("Neither id nor the entity is specified. We need exactly single input for this command") if id and entity: raise ValueError("Both id and entity is specified. We need exactly single input for this command") try: result = client.get_alert(id=id, entity=entity) except Exception as exception: raise Exception(f"No alert found with the given parameters :- {str(exception)}") title = "Alert Summary" human_readable = tableToMarkdown(title, result, removeNull=True) return CommandResults( outputs_prefix="Doppel.Alert", outputs_key_field="id", outputs=result, readable_output=human_readable, raw_response=result, ) def doppel_update_alert_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Executes the update alert command. :param client: The Client instance. :param args: Command arguments. :return: CommandResults object. """ demisto.debug(f"Update Alert cmd params: {args}") alert_id = args.get("alert_id", "") entity = args.get("entity", "") queue_state = args.get("queue_state", "") entity_state = args.get("entity_state", "") comment = args.get("comment", "") if alert_id and entity: raise ValueError("Only one of 'alert_id' or 'entity' can be specified.") if not any([queue_state, entity_state, comment]): raise ValueError("At least one of 'queue_state', 'entity_state', or 'comment' must be provided.") try: result = client.update_alert( queue_state=queue_state, entity_state=entity_state, alert_id=alert_id, entity=entity, comment=comment ) except Exception as exception: raise Exception(f"Failed to update the alert with the given parameters :- {str(exception)}.") title = "Alert Summary" human_readable = tableToMarkdown(title, result, removeNull=True) return CommandResults( outputs_prefix="Doppel.UpdatedAlert", outputs_key_field="id", outputs=result, readable_output=human_readable, raw_response=result, ) def format_datetime(timestamp_str): """ Formats a given timestamp string into ISO 8601 format. :param timestamp_str: A string representing the datetime, which may or may not be in ISO 8601 format. :return: A formatted datetime string in ISO 8601 format (YYYY-MM-DDTHH:MM:SS). """ if not timestamp_str: return None # Return None if no timestamp is provided try: # Replace 'Z' with '+00:00' to make it compatible with fromisoformat() if timestamp_str.endswith("Z"): timestamp_str = timestamp_str.replace("Z", "+00:00") # Attempt to parse the string in ISO 8601 format datetime.fromisoformat(timestamp_str) return timestamp_str # Already in ISO format except ValueError: datetime_obj = arg_to_datetime(timestamp_str) # Convert to standard ISO 8601 format without microseconds and timezone iso_format_truncated = datetime_obj.strftime("%Y-%m-%dT%H:%M:%S.%fZ") if datetime_obj else None return iso_format_truncated def doppel_get_alerts_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Command to fetch multiple alerts based on query parameters. :param client: Client instance to interact with the API. :param args: Command arguments containing the query parameters as key-value pairs. :return: CommandResults object with the retrieved alerts. """ created_before = format_datetime(args.get("created_before")) created_after = format_datetime(args.get("created_after")) # Extract query parameters directly from arguments query_params = { "search_key": args.get("search_key"), "queue_state": args.get("queue_state"), "product": args.get("product"), "created_before": created_before, "created_after": created_after, "sort_type": args.get("sort_type"), "sort_order": args.get("sort_order"), "page": args.get("page"), "tags": argToList(args.get("tags"), separator=",", transform=None), } # Call the client's `get_alerts` method to fetch data demisto.debug(f"Query parameters before sending to client: {query_params}") try: results = client.get_alerts(params=query_params) except Exception as exception: raise Exception(f"No alerts were found with the given parameters :- {str(exception)}.") demisto.debug(f"Results received: {results}") alerts = results.get("alerts") title = "Alert Summary" human_readable = tableToMarkdown(title, alerts, removeNull=True) return CommandResults( outputs_prefix="Doppel.GetAlerts", outputs_key_field="id", outputs=results, readable_output=human_readable, raw_response=results, ) def doppel_create_alert_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Comand to create an alert in the Doppel client using the provided arguments. :param client: Client instance to interact with the API. :param args: Command arguments containing the query parameters as key-value pairs. :return: CommandResults object including details of the created alert. """ entity = args.get("entity") if not entity: raise ValueError("Entity must be specified to create an alert.") try: result = client.create_alert(entity=entity) except Exception as exception: raise Exception(f"Failed to create the alert with the given parameters:- {str(exception)}.") title = "Alert Summary" human_readable = tableToMarkdown(title, result, removeNull=True) return CommandResults( outputs_prefix="Doppel.CreatedAlert", outputs_key_field="id", outputs=result, readable_output=human_readable, raw_response=result, ) def doppel_create_abuse_alert_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Comand to create an abuse alert in the Doppel client using the provided arguments. :param client: Client instance to interact with the API. :param args: Command arguments containing the query parameters as key-value pairs. :return: CommandResults object including details of the created abuse alert. """ entity = args.get("entity") if not entity: raise ValueError("Entity must be specified to create an abuse alert.") try: result = client.create_abuse_alert(entity=entity) except Exception as exception: raise Exception(f"Failed to create the abuse alert with the given parameters:- {str(exception)}.") title = "Alert Summary" human_readable = tableToMarkdown(title, result, removeNull=True) return CommandResults( outputs_prefix="Doppel.AbuseAlert", outputs_key_field="id", outputs=result, readable_output=human_readable, raw_response=result, ) def _parse_fetch_timeout(): """Parse the fetch_timeout param. Blank or invalid means no timeout limit.""" raw = demisto.params().get("fetch_timeout") if raw is None or str(raw).strip() == "": return None try: return float(raw) except (TypeError, ValueError): return None def _parse_max_fetch(): """Parse the max_fetch param, falling back to a safe default when blank/invalid.""" raw = demisto.params().get("max_fetch") try: value = int(raw) except (TypeError, ValueError): return DEFAULT_MAX_FETCH return value if value > 0 else DEFAULT_MAX_FETCH def _incident_alert_id(incident): """Return the Doppel alert id for a queued/created incident (dbotMirrorId, else rawJSON id).""" alert_id = str(incident.get("dbotMirrorId") or "") if alert_id: return alert_id raw = incident.get("rawJSON") if raw: try: return str(json.loads(raw).get("id") or "") except Exception: return "" return "" def _xsoar_severity(alert): """Map a Doppel alert severity string to an XSOAR incident severity (0 = Unknown).""" return SEVERITY_MAP.get(str(alert.get("severity") or "").strip().lower(), 0) def _alert_to_incident(alert, mirroring_object): """Build an XSOAR incident dict from a single Doppel alert.""" alert_id = str(alert.get("id") or "") created_at_str = alert.get("created_at") created_at_datetime = None if created_at_str: for date_format in (DOPPEL_PAYLOAD_DATE_FORMAT, DOPPEL_API_DATE_FORMAT): try: created_at_datetime = datetime.strptime(created_at_str, date_format).replace(tzinfo=UTC) break except (ValueError, TypeError): continue if created_at_datetime is None: created_at_datetime = datetime.now(UTC) severity = _xsoar_severity(alert) # Shape entity_content for the grid incident field before it is mapped from rawJSON. if "entity_content" in alert: alert["entity_content"] = _normalize_entity_content_for_grid(alert.get("entity_content")) alert.update(mirroring_object) # Use the external Doppel alert id (e.g. TET-1953421) as the incident name so it # is human-meaningful and duplicates are visually obvious. incident_name = f"Doppel Alert {alert_id}" if alert_id else "Doppel Alert" return { "name": incident_name, "type": DOPPEL_ALERT, "occurred": created_at_datetime.strftime(XSOAR_DATE_FORMAT), "severity": severity, "dbotMirrorId": alert_id, "rawJSON": json.dumps(alert), } def fetch_incidents_command(client: Client, args: dict[str, Any]) -> None: """ Fetch incidents from Doppel alerts and create XSOAR incidents. Alerts are pulled page by page from the last-run cursor in ascending order and de-duplicated by their Doppel alert id (both within a single run and across runs via a small boundary set), so a large multi-page backlog is drained safely over consecutive runs without ever creating the same alert twice. Only lightweight cursor metadata is persisted in lastRun - never raw incident data. """ demisto.debug("Doppel - Fetching alerts from Doppel.") start_time = time.time() timeout = _parse_fetch_timeout() fetch_limit = _parse_max_fetch() last_run = demisto.getLastRun() or {} demisto.debug(f"Doppel - Last run details: {last_run}") recently_seen_ids = last_run.get("recently_seen_ids", []) last_run_time = last_run.get("last_run", None) last_fetch_datetime = _get_last_fetch_datetime(last_run_time) # Seed the dedupe set with the ids persisted from the previous run's high-water-mark # second. Because the next run's cursor re-pull is inclusive of that second, this # prevents re-creating alerts that were already emitted at the boundary second. seen_alert_ids = {str(i) for i in recently_seen_ids if i} mirroring_object = _get_mirroring_fields() incidents: list[dict[str, Any]] = [] page = 0 while len(incidents) < fetch_limit: if timeout is not None and (time.time() - start_time) > timeout: demisto.debug("Doppel - Fetch reached its time budget; progress saved, the next run continues.") break # Hard safeguard against an unbounded loop (e.g. a misbehaving API that never # returns an empty page) when no fetch timeout is configured. if page >= MAX_FETCH_PAGES_PER_RUN: demisto.debug(f"Doppel - Reached the per-run page ceiling ({MAX_FETCH_PAGES_PER_RUN}); continuing next run.") break alerts = _paginated_call_to_get_alerts(client, page, last_fetch_datetime) if not alerts: demisto.debug("Doppel - No more alerts returned; exiting pagination loop.") break for alert in alerts: alert_id = str(alert.get("id") or "") if not alert_id or alert_id in seen_alert_ids: continue seen_alert_ids.add(alert_id) incidents.append(_alert_to_incident(alert, mirroring_object)) if len(incidents) >= fetch_limit: break demisto.debug(f"Doppel - Fetched page {page}; collected {len(incidents)} new alert(s) so far.") page += 1 # Advance the cursor only to the newest alert we are actually creating this run # (never past it), so alerts fetched-but-not-created are simply re-pulled next run # instead of being buffered in lastRun. We do NOT skip the boundary second; instead # we persist the ids emitted at that second so the inclusive re-pull on the next run # is de-duplicated rather than lost. occurred_times = [str(inc["occurred"]) for inc in incidents if inc.get("occurred")] if occurred_times: newest_occurred = max(occurred_times) next_fetch = newest_occurred boundary = {_incident_alert_id(inc) for inc in incidents if inc.get("occurred") == newest_occurred} boundary.discard("") # While the cursor stays on the same second across runs, carry forward the ids # already created on prior runs so the inclusive re-pull never re-creates them. if last_run_time == newest_occurred: boundary.update(str(i) for i in recently_seen_ids if i) boundary_ids = list(boundary) else: next_fetch = last_run_time boundary_ids = [str(i) for i in recently_seen_ids if i] demisto.setLastRun( { "last_run": next_fetch, "recently_seen_ids": boundary_ids, } ) demisto.debug(f"Doppel - Next cursor: {next_fetch}; boundary ids: {len(boundary_ids)}.") # Create incidents in XSOAR (an empty list still registers the run). try: demisto.incidents(incidents) demisto.debug(f"Doppel - Created {len(incidents)} incident(s) in XSOAR.") except Exception as e: raise ValueError(f"Incident creation failed due to: {str(e)}") def get_modified_remote_data_command(client: Client, args: dict[str, Any]) -> GetModifiedRemoteDataResponse: """ Checks for remote modifications since the last update timestamp and returns a list of modified incident IDs. """ remote_args = GetModifiedRemoteDataArgs(args) try: last_update_datetime = arg_to_datetime(remote_args.last_update, required=False) except ValueError: last_update_datetime = None if not last_update_datetime: raise DemistoException(f"Doppel - Could not parse the lastUpdate timestamp: {remote_args.last_update!r}") last_update = last_update_datetime.strftime(DOPPEL_API_DATE_FORMAT) try: # Page through the full set of modified alerts. Taking only the first page (the # previous behavior) silently dropped changes on busy tenants, because the server # advances lastUpdate after every cycle regardless of what was returned. modified_incident_ids: list[str] = [] seen_ids: set[str] = set() page = 0 while page < MAX_FETCH_PAGES_PER_RUN: query_params = { "last_activity_timestamp": last_update, "page": page, "page_size": DOPPEL_MAX_PAGE_SIZE, } results = client.get_alerts(params=query_params) alerts = results.get("alerts", []) if not alerts: break for alert in alerts: alert_id = str(alert.get("id") or "") if alert_id and alert_id not in seen_ids: seen_ids.add(alert_id) modified_incident_ids.append(alert_id) if len(alerts) < DOPPEL_MAX_PAGE_SIZE: break page += 1 demisto.debug(f"Found {len(modified_incident_ids)} modified remote incidents. Incidents: {modified_incident_ids}") return GetModifiedRemoteDataResponse(modified_incident_ids) except Exception as e: demisto.error(f"Error in get-modified-remote-data: {e}") return GetModifiedRemoteDataResponse([]) def get_remote_data_command(client: Client, args: dict[str, Any]) -> GetRemoteDataResponse: try: remote_updated_incident_data: dict[str, Any] = {} mirrored_object: dict[str, Any] = {} demisto.debug(f'Calling the "get-remote-data" for {args["id"]}') parsed_args = GetRemoteDataArgs(args) remote_updated_incident_data, parsed_entries = _get_remote_updated_incident_data_with_entry( client, parsed_args.remote_incident_id, parsed_args.last_update ) if remote_updated_incident_data: demisto.debug(f'Found updates in the alert with id: {args["id"]}') return GetRemoteDataResponse(remote_updated_incident_data, parsed_entries) else: demisto.debug(f"Nothing new in the incident {parsed_args.remote_incident_id}") return GetRemoteDataResponse(mirrored_object, entries=[{}]) except Exception as e: demisto.error(f"Error while running get_remote_data_command: {e}") if "Rate limit exceeded" in str(e): demisto.debug("API rate limit") if not remote_updated_incident_data: remote_updated_incident_data = {"id": parsed_args.remote_incident_id} mirrored_object["in_mirror_error"] = str(e) return GetRemoteDataResponse(mirrored_object, entries=[]) def update_remote_system_command(client: Client, args: dict[str, Any]) -> str: """update-remote-system command: pushes local changes to the remote system Outgoing mirroring only archives the Doppel alert when the XSOAR incident is closed. ``entity_state`` is always read from the live Doppel alert so it is preserved even when the outgoing mapper does not include it. Close notes from XSOAR are sent as the comment. :type client: ``Client`` :param client: XSOAR client to use :type args: ``dict[str, Any]`` :param args: all command arguments, usually passed from ``demisto.args()``. ``args['data']`` the data to send to the remote system ``args['entries']`` the entries to send to the remote system ``args['incidentChanged']`` boolean telling us if the local incident indeed changed or not ``args['remoteId']`` the remote incident id :return: ``str`` containing the remote incident id - really important if the incident is newly created remotely :rtype: ``str`` """ parsed_args = UpdateRemoteSystemArgs(args) remote_incident_id = parsed_args.remote_incident_id # Only update Doppel when the XSOAR incident is closed (status DONE). # This command is event-driven on local incident changes; it is not polled every minute. if parsed_args.inc_status != IncidentStatus.DONE: demisto.debug(f"Incident not closed. Skipping update for remote ID [{remote_incident_id}].") return remote_incident_id if not remote_incident_id: demisto.debug("Doppel - No remote incident id; skipping outgoing update.") return remote_incident_id demisto.debug(f"Closing remote Doppel alert [{remote_incident_id}] (XSOAR incident closed).") try: # Always fetch the live alert so entity_state comes from Doppel, not from the # outgoing mapper (which currently only maps queue_state). current_alert = client.get_alert(id=remote_incident_id, entity="") or {} entity_state = current_alert.get("entity_state") or "" delta = parsed_args.delta or {} data = parsed_args.data or {} comment = delta.get("closeNotes") or data.get("closeNotes") or "" already_archived = current_alert.get("queue_state") == "archived" if already_archived and not comment: demisto.debug(f"Doppel alert [{remote_incident_id}] already archived with no close notes; skipping.") return remote_incident_id client.update_alert( queue_state="archived", entity_state=entity_state, comment=comment, alert_id=remote_incident_id, ) # Log only whether close notes were sent, not their content (analyst-authored text). demisto.debug( f"Doppel - Archived remote alert [{remote_incident_id}] " f"with entity_state={entity_state!r} comment_sent={bool(comment)}." ) except Exception as e: demisto.error( f"Doppel - Error in outgoing mirror for incident {remote_incident_id} " f"\nError message: {str(e)}\n{traceback.format_exc()}" ) return remote_incident_id def get_mapping_fields_command(client: Client, args: dict[str, Any]) -> GetMappingFieldsResponse: """ Retrieves the mapping fields for Doppel alerts in XSOAR. This function defines a custom mapping for Doppel alerts, adding specific fields that can be used for incident mirroring and enrichment in Cortex XSOAR. Args: client (Client): The API client used to communicate with Doppel. args (dict[str, Any]): Command arguments (not used in this function). Returns: GetMappingFieldsResponse: The mapping response containing field definitions. """ xdr_incident_type_scheme = SchemeTypeMapping(type_name=DOPPEL_ALERT) doppel_fields = { "id": "Unique identifier of the alert", "entity": "URL or profile link related to the alert", "brand": "Brand associated with the alert", "queue_state": "Queue State of the Doppel Alert", "entity_state": "Current state of the alert entity", "severity": "Severity level of the alert", "product": "Product category associated with the alert", "platform": "Platform on which the alert was generated", "source": "Source from which the alert was generated", "notes": "Additional notes related to the alert", "created_at": "Timestamp when the alert was created", "screenshot_url": "URL of the alert screenshot when available", "last_activity": "Timestamp of the last activity on the alert", "score": "Score assigned to the alert", "message": "Message associated with the alert", "assignee": "User assigned to the alert", "doppel_link": "Link to the alert in the Doppel platform", "uploaded_by": "User who uploaded the alert", "entity_content": "Additional content related to the alert entity", "audit_logs": "Audit log entries for the alert", "tags": "Tags associated with the alert", "alert_summary": "Summary of the alert", } for field_name, description in doppel_fields.items(): xdr_incident_type_scheme.add_field(name=field_name, description=description) mapping_response = GetMappingFieldsResponse() mapping_response.add_scheme_type(xdr_incident_type_scheme) demisto.debug(f"Mapping fields response created: {mapping_response}") return mapping_response """ MAIN FUNCTION """ def main() -> None: """Main function, parses params and runs command functions.""" api_key = demisto.params().get("credentials", {}).get("password") user_api_key = demisto.params().get("user_credentials", {}).get("password") organization_code = demisto.params().get("organization_code") verify = not demisto.params().get("insecure") proxy = demisto.params().get("proxy") demisto.debug(f"Verify SSL: {verify} and Proxy: {proxy}") # Get the service API URL base_url = urljoin(demisto.params()["url"], "/v1") # Explicitly define the type for the command function dictionary supported_commands: dict[str, Callable[[Client, dict[str, Any]], Any]] = { "fetch-incidents": fetch_incidents_command, "get-modified-remote-data": get_modified_remote_data_command, "get-remote-data": get_remote_data_command, "update-remote-system": update_remote_system_command, "get-mapping-fields": get_mapping_fields_command, "doppel-get-alert": doppel_get_alert_command, "doppel-update-alert": doppel_update_alert_command, "doppel-get-alerts": doppel_get_alerts_command, "doppel-create-alert": doppel_create_alert_command, "doppel-create-abuse-alert": doppel_create_abuse_alert_command, } # Special case for 'test-module' which does not take args supported_commands_test_module: dict[str, Callable[[Client], Any]] = {"test-module": test_module} current_command: str = demisto.command() demisto.info(f"Command being called is {current_command}") try: client = Client( base_url=base_url, api_key=api_key, user_api_key=user_api_key, organization_code=organization_code, verify=verify, proxy=proxy, ) if current_command in supported_commands_test_module: # Calls test_module(client) without args result = supported_commands_test_module[current_command](client) elif current_command in supported_commands: # Calls command_function(client, demisto.args()) result = supported_commands[current_command](client, demisto.args()) else: demisto.error(f"Command is not implemented: {current_command}") raise NotImplementedError(f"The {current_command} command is not supported") demisto.info(f"Command run successful: {current_command}") return_results(result) except Exception as e: return_error(f"Failed to execute {current_command} command.\nError:\n{str(e)}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()