XSOAR Mirroring
Facilitates mirroring of Cortex XSOAR incidents between different Cortex XSOAR tenants.
Utilities · XSOAR Mirroring
Details
| ID | XSOAR Mirroring |
|---|---|
| Provider | Palo Alto Networks |
| Category | Utilities |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10230415 |
README
Facilitates mirroring of Cortex XSOAR incidents between different Cortex XSOAR tenants.
This integration is compatible with Cortex XSOAR versions 6.x and 8.x, and it has been tested for interoperability across the range of Cortex XSOAR versions from 6.12 and lower and 8.4 and lower.
Configure XSOAR Mirroring on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for XSOAR Mirroring.
-
Click Add instance to create and configure a new integration instance.
Parameter Description Required Incident type False XSOAR Server URL The URL of the Cortex XSOAR server to which you are connecting. True API Key The API key to access the server. The key must be provided by the server to which you are connecting. False API Key ID The API key to access the server. The key must be provided by the server to which you are connecting. When the target server is XSOAR 8.X, the API Key ID is required as well (not relevant for XSOAR 6.X). False Fetch incidents False Maximum number of incidents to pull per fetch False Fetch only incidents that match the query Don’t add created time to the query as this field will be addressed in the “First fetch time”. False First fetch time Date or relative timestamp to start fetching incidents from, in the format of <number> <time unit>. For example, 2 minutes, 12 hours, 6 days, 2 weeks, 3 months, 1 year, ISO timestamp. Default is 3 days. False Entry Categories Which entries to retrieve from the Cortex XSOAR server. The available options are notes, comments (chats), and files. (attachments). False Incoming Entry tags Only entries with these tags are retrieved from the Cortex XSOAR server. If no tags are listed, no entries are retrieved. False Outgoing Entry Tags Choose the tags to filter the entries you want to send to the other Cortex XSOAR instance. If no tags are listed, no entries will be sent. False Incident Mirroring Direction False Disable fetching for incidents that came from this integration Enable this option to disable mirroring of incidents that came from the integration of XSOAR Mirroring. This adds `-sourceBrand:“XSOAR Mirroring”` to your query. False Trust any certificate (not secure) False Use system proxy settings False Debug mode (will print debug logs to info) False Mirror Playbook ID A parameter integration that removes the playbook ID field from incoming incidents. Note: When set to true (default), the instance will attempt to run a playbook according to the incoming ID. When set to false, the instance will run the default playbook for the incident type (if configured locally). False Fetch incident history Will mirror historical notes,tags and attachments in case their corresponding incidents were deleted. Notice can impact performance if combined with “Reset the “last run” timestamp” and multiple incidents in system. False - To set up the mirroring, enable Fetching incidents in your instance configuration.
-
In the Incident Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:
| Option | Description |
| — | — |
| None | Turns off incident mirroring. |
| Incoming | Any changes in XSOAR Mirroring events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents. |
| Outgoing | Any changes in Cortex XSOAR incidents will be reflected in XSOAR Mirroring events (outgoing mirrored fields). |
| Incoming And Outgoing | Changes in Cortex XSOAR incidents and XSOAR Mirroring events 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 XSOAR Mirroring.
- Click Test to ensure that you can communicate with the Cortex XSOAR tenant.
Important notes
- In order to mirror custom fields, you need to create an incoming mapper for the integration and explicitly specify them in it.
- In order to mirror custom fields in both directions, the custom fields in both Cortex XSOAR instances must have the same CLI name.
- Mirrored incidents include the playbook ID. The receiving side will attempt to run a playbook with a matching ID, if one exists locally. To have the machine run the default playbook for the mirrored incident, set the
Mirror Playbook IDtofalse. Otherwise (default), the machine will attempt to run a playbook whose ID matches theplaybookIdfield in the mirrored incident.
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.
xsoar-search-incidents
Search remote Cortex XSOAR for incidents.
Base Command
xsoar-search-incidents
Input
| Argument Name | Description | Required |
|---|---|---|
| query | Which incidents to retrieve. Default is -status:closed -category:job. | Optional |
| start_time | From when to search. Default is 3 days. | Optional |
| max_results | How many incidents to retrieve. Default is 10. | Optional |
| columns | Which columns to display. Default is ‘id,name,type,status,severity,owner,occured’. To display all columns, insert ‘all’. Default is id,name,type,status,severity,owner,occured. | Optional |
Context Output
There is no context output for this command.
Command example
!xsoar-search-incidents query="-status:closed -category:job"
Context Example
{
"XSOAR": {
"Incident": {
"CustomFields": {
"containmentsla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 30,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"detectionsla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 20,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"endpoint": [
{}
],
"filerelationships": [
{},
{},
{}
],
"isactive": "true",
"numberofrelatedincidents": 0,
"numberofsimilarfiles": 0,
"remediationsla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 7200,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"similarincidentsdbot": [
{}
],
"timetoassignment": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 0,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"triagesla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 30,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"urlsslverification": []
},
"account": "",
"activated": "0001-01-01T00:00:00Z",
"attachment": null,
"autime": 1678010808446441500,
"cacheVersn": 0,
"canvases": null,
"category": "",
"changeStatus": "new",
"closeNotes": "",
"closeReason": "",
"closed": "0001-01-01T00:00:00Z",
"closingUserId": "",
"created": "2023-03-05T10:06:48.446441591Z",
"dbotCreatedBy": "admin",
"dbotCurrentDirtyFields": null,
"dbotDirtyFields": null,
"dbotMirrorDirection": "",
"dbotMirrorId": "",
"dbotMirrorInstance": "",
"dbotMirrorLastSync": "0001-01-01T00:00:00Z",
"dbotMirrorTags": null,
"details": "This is the new details",
"droppedCount": 0,
"dueDate": "2023-03-15T10:06:48.446441591Z",
"feedBased": false,
"id": "4",
"insights": 0,
"investigationId": "4",
"isDebug": false,
"isPlayground": false,
"labels": [
{
"type": "Instance",
"value": "admin"
},
{
"type": "Brand",
"value": "Manual"
}
],
"lastJobRunTime": "0001-01-01T00:00:00Z",
"lastOpen": "0001-01-01T00:00:00Z",
"linkedCount": 0,
"linkedIncidents": null,
"modified": "2023-03-05T14:43:22.319158041Z",
"name": "testing",
"notifyTime": "2023-03-05T10:06:50.261155172Z",
"occurred": "2023-03-05T10:06:48.446441435Z",
"openDuration": 0,
"owner": "admin",
"parent": "",
"phase": "",
"playbookId": "playbook0",
"rawCategory": "",
"rawCloseReason": "",
"rawJSON": "",
"rawName": "testing",
"rawPhase": "",
"rawType": "Unclassified",
"reason": "",
"reminder": "0001-01-01T00:00:00Z",
"runStatus": "waiting",
"severity": 0,
"sla": 0,
"sortValues": [],
"sourceBrand": "Manual",
"sourceInstance": "admin",
"status": 1,
"type": "Unclassified",
"version": 13
}
}
}
Human Readable Output
Search Results
id name type status severity owner occured 4 testing Unclassified 1 0 admin
xsoar-get-incident
Retrieve incident and entries from the remote Cortex XSOAR server.
Base Command
xsoar-get-incident
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The remote incident ID. | Required |
| from_date | Retrieve entries that were created after the last update. Default is 3 days. | Optional |
| categories | Retrieve only the entries from these categories. Default is chats,notes. | Optional |
| tags | Only entries with these tags are retrieved from the Cortex XSOAR server. If no tags are listed, no entries are retrieved. | Optional |
| max_results | Max number of entries to retrieve. Default is 10. | Optional |
Context Output
There is no context output for this command.
Command example
!xsoar-get-incident id=4
Context Example
{
"XSOAR": {
"Incident": {
"CustomFields": {
"containmentsla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 30,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"detectionsla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 20,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"endpoint": [
{}
],
"filerelationships": [
{},
{},
{}
],
"isactive": "true",
"numberofrelatedincidents": 0,
"numberofsimilarfiles": 0,
"remediationsla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 7200,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"similarincidentsdbot": [
{}
],
"timetoassignment": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 0,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"triagesla": {
"accumulatedPause": 0,
"breachTriggered": false,
"dueDate": "0001-01-01T00:00:00Z",
"endDate": "0001-01-01T00:00:00Z",
"lastPauseDate": "0001-01-01T00:00:00Z",
"runStatus": "idle",
"sla": 30,
"slaStatus": -1,
"startDate": "0001-01-01T00:00:00Z",
"totalDuration": 0
},
"urlsslverification": []
},
"account": "",
"activated": "0001-01-01T00:00:00Z",
"attachment": null,
"autime": 1678010808446441500,
"cacheVersn": 0,
"canvases": null,
"category": "",
"closeNotes": "",
"closeReason": "",
"closed": "0001-01-01T00:00:00Z",
"closingUserId": "",
"created": "2023-03-05T10:06:48.446441591Z",
"dbotCreatedBy": "admin",
"dbotCurrentDirtyFields": null,
"dbotDirtyFields": null,
"dbotMirrorDirection": "",
"dbotMirrorId": "",
"dbotMirrorInstance": "",
"dbotMirrorLastSync": "0001-01-01T00:00:00Z",
"dbotMirrorTags": null,
"details": "This is the new details",
"droppedCount": 0,
"dueDate": "2023-03-15T10:06:48.446441591Z",
"feedBased": false,
"id": "4",
"investigationId": "4",
"isDebug": false,
"isPlayground": false,
"labels": [
{
"type": "Instance",
"value": "admin"
},
{
"type": "Brand",
"value": "Manual"
}
],
"lastJobRunTime": "0001-01-01T00:00:00Z",
"lastOpen": "0001-01-01T00:00:00Z",
"linkedCount": 0,
"linkedIncidents": null,
"modified": "2023-03-05T14:43:22.319158041Z",
"name": "testing",
"notifyTime": "2023-03-05T10:06:50.261155172Z",
"occurred": "2023-03-05T10:06:48.446441435Z",
"openDuration": 0,
"owner": "admin",
"parent": "",
"phase": "",
"playbookId": "playbook0",
"rawCategory": "",
"rawCloseReason": "",
"rawJSON": "",
"rawName": "testing",
"rawPhase": "",
"rawType": "Unclassified",
"reason": "",
"reminder": "0001-01-01T00:00:00Z",
"runStatus": "waiting",
"severity": 0,
"sla": 0,
"sourceBrand": "Manual",
"sourceInstance": "admin",
"status": 1,
"type": "Unclassified",
"version": 13
}
}
}
Human Readable Output
Incident testing
CustomFields account activated attachment autime cacheVersn canvases category closeNotes closeReason closed closingUserId created dbotCreatedBy dbotCurrentDirtyFields dbotDirtyFields dbotMirrorDirection dbotMirrorId dbotMirrorInstance dbotMirrorLastSync dbotMirrorTags details droppedCount dueDate feedBased id investigationId isDebug isPlayground labels lastJobRunTime lastOpen linkedCount linkedIncidents modified name notifyTime occurred openDuration owner parent phase playbookId rawCategory rawCloseReason rawJSON rawName rawPhase rawType reason reminder runStatus severity sla sourceBrand sourceInstance status type version containmentsla: {“accumulatedPause”: 0, “breachTriggered”: false, “dueDate”: “0001-01-01T00:00:00Z”, “endDate”: “0001-01-01T00:00:00Z”, “lastPauseDate”: “0001-01-01T00:00:00Z”, “runStatus”: “idle”, “sla”: 30, “slaStatus”: -1, “startDate”: “0001-01-01T00:00:00Z”, “totalDuration”: 0}
detectionsla: {“accumulatedPause”: 0, “breachTriggered”: false, “dueDate”: “0001-01-01T00:00:00Z”, “endDate”: “0001-01-01T00:00:00Z”, “lastPauseDate”: “0001-01-01T00:00:00Z”, “runStatus”: “idle”, “sla”: 20, “slaStatus”: -1, “startDate”: “0001-01-01T00:00:00Z”, “totalDuration”: 0}
endpoint: {}
filerelationships: {},
{},
{}
isactive: true
numberofrelatedincidents: 0
numberofsimilarfiles: 0
remediationsla: {“accumulatedPause”: 0, “breachTriggered”: false, “dueDate”: “0001-01-01T00:00:00Z”, “endDate”: “0001-01-01T00:00:00Z”, “lastPauseDate”: “0001-01-01T00:00:00Z”, “runStatus”: “idle”, “sla”: 7200, “slaStatus”: -1, “startDate”: “0001-01-01T00:00:00Z”, “totalDuration”: 0}
similarincidentsdbot: {}
timetoassignment: {“accumulatedPause”: 0, “breachTriggered”: false, “dueDate”: “0001-01-01T00:00:00Z”, “endDate”: “0001-01-01T00:00:00Z”, “lastPauseDate”: “0001-01-01T00:00:00Z”, “runStatus”: “idle”, “sla”: 0, “slaStatus”: -1, “startDate”: “0001-01-01T00:00:00Z”, “totalDuration”: 0}
triagesla: {“accumulatedPause”: 0, “breachTriggered”: false, “dueDate”: “0001-01-01T00:00:00Z”, “endDate”: “0001-01-01T00:00:00Z”, “lastPauseDate”: “0001-01-01T00:00:00Z”, “runStatus”: “idle”, “sla”: 30, “slaStatus”: -1, “startDate”: “0001-01-01T00:00:00Z”, “totalDuration”: 0}
urlsslverification:0001-01-01T00:00:00Z 1678010808446441591 0 0001-01-01T00:00:00Z 2023-03-05T10:06:48.446441591Z admin 0001-01-01T00:00:00Z This is the new details 0 2023-03-15T10:06:48.446441591Z false 4 4 false false {‘value’: ‘admin’, ‘type’: ‘Instance’},
{‘value’: ‘Manual’, ‘type’: ‘Brand’}0001-01-01T00:00:00Z 0001-01-01T00:00:00Z 0 2023-03-05T14:43:22.319158041Z testing 2023-03-05T10:06:50.261155172Z 2023-03-05T10:06:48.446441435Z 0 admin playbook0 testing Unclassified 0001-01-01T00:00:00Z waiting 0 0 Manual admin 1 Unclassified 13 Last entries since 2023-03-02T14:44:47.000Z
brand cacheVersn category contents contentsSize created cronView dbotCreatedBy endingDate format id incidentCreationTime investigationId isTodo mirrored modified note parentEntryTruncated pinned recurrent reputationSize retryTime scheduled startDate times timezoneOffset type user version none 0 chat done 4 2023-03-05T10:38:21.895125832Z false admin 0001-01-01T00:00:00Z text 52@4 0001-01-01T00:00:00Z 4 false false 2023-03-05T10:38:21.895153311Z false false false false 0 0001-01-01T00:00:00Z false 0001-01-01T00:00:00Z 0 0 1 admin 1
get-remote-data
Get remote data from a remote incident. Note that this method will not update the current incident. It is used for debugging purposes only.
Base Command
get-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The remote incident ID. | Required |
| lastUpdate | Retrieve entries that were created after the last update. | Optional |
Context Output
There is no context output for this command.
get-modified-remote-data
Returns a list of incident IDs that were modified since the last sync. This command is used to enable incremental mirroring and avoid full blind syncs.
Note: This command is supported only for Cortex XSOAR version 8.15 and later.
Base Command
get-modified-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| lastUpdate | ISO8601 timestamp of the last time the integration was synced. Only incidents modified after this time are returned. | Required |
Context Output
There are no context outputs for this command.
get-mapping-fields
Retrieves the mapping schema from a remote incident.
Base Command
get-mapping-fields
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
There is no context output for this command.
Incident Mirroring
You can enable incident mirroring between Cortex XSOAR incidents and XSOAR Mirroring corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:
- 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 XSOAR Mirroring events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents. Outgoing Any changes in Cortex XSOAR incidents will be reflected in XSOAR Mirroring events (outgoing mirrored fields). Incoming And Outgoing Changes in Cortex XSOAR incidents and XSOAR Mirroring events 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 XSOAR Mirroring.
Configuration parameters
incidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalurl— XSOAR Server URL (required)apikey— API Keycredentials_api_key— API Key ID (for XSOAR 8.x only)isFetch— Fetch incidentsmax_fetch— Maximum number of incidents to pull per fetchquery— Fetch only incidents that match the queryfirst_fetch— First fetch timecategories— Entry Categoriestags— Incoming Entry tagsmirror_tag— Outgoing Entry Tagsmirror_direction— Incident Mirroring Directiondisable_from_same_integration— Disable fetching for incidents that came from this integrationinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsdebug_mode— Debug mode (will print debug logs to info)mirror_playbook_id— Mirror Playbook IDfetch_incident_history— Fetch incident history
Commands (5)
-
get-mapping-fieldsRetrieves the mapping schema from a remote incident.
-
get-modified-remote-dataReturns a list of incident IDs that were modified since the last sync. Used to enable incremental mirroring and avoid full blind syncs.
-
get-remote-dataGet remote data from a remote incident. Note that this method will not update the current incident. It is used for debugging purposes only.
-
xsoar-get-incidentRetrieve incident and entries from the remote Cortex XSOAR server.
-
xsoar-search-incidentsSearch remote Cortex XSOAR for incidents.
from datetime import datetime, timedelta, UTC import dateparser import pytest from CommonServerPython import DemistoException, EntryType from XSOARmirroring import ( MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY, XSOAR_DATE_FORMAT, Client, fetch_incidents, get_mapping_fields_command, get_modified_remote_data_command, update_remote_system_command, validate_and_prepare_basic_params, ) def generate_dummy_client(): class Client: def __init__(self): pass def get_incident_fields(self): pass def get_incident_types(self): pass def get_incident(self): pass def update_incident(self): pass return Client INCIDENT_FIELDS = [ { "group": 0, "associatedToAll": True, "name": "field1", "type": "type1", "description": "description1", "cliName": "cliName1", "content": False, "system": True, }, { "group": 0, "associatedTypes": ["test"], "name": "field2", "type": "type2", "description": "description2", "cliName": "cliName2", "content": True, "system": True, }, ] INCIDENT_TYPES = [{"name": "Something"}, {"name": "test"}] def test_mirroring(mocker): """ Given: - Two incident types and fields. When: - one field is associated to all while the second is associated to one. Then: - A correct mapping dict is created, with a "Default Scheme" included """ client = generate_dummy_client() mocker.patch.object(client, "get_incident_fields", return_value=INCIDENT_FIELDS) mocker.patch.object(client, "get_incident_types", return_value=INCIDENT_TYPES) response = get_mapping_fields_command(client).extract_mapping() assert len(response) == 3 assert "Default Mapping" in str(response) assert response["Default Mapping"] == {"cliName1": "field1 - type1"} assert response["test"] == {"CustomFields": {"cliName2": "field2 - type2"}, "cliName1": "field1 - type1"} assert response["Something"] == {"cliName1": "field1 - type1"} INCIDENTS = [ {"id": 1, "created": (datetime.now() - timedelta(minutes=10)).strftime(XSOAR_DATE_FORMAT)}, {"id": 2, "created": (datetime.now() - timedelta(minutes=8)).strftime(XSOAR_DATE_FORMAT)}, {"id": 3, "created": (datetime.now() - timedelta(minutes=5)).strftime(XSOAR_DATE_FORMAT)}, ] INCIDENTS_IN_CONTEXT = { "XSOARMirror_mirror_reset": { 4: True, 5: True, 6: True, } } INCIDENTS_MIRRORING_PLAYBOOK_ID = [ {"id": 1, "created": (datetime.now() - timedelta(minutes=10)).strftime(XSOAR_DATE_FORMAT), "playbookId": "test"} ] REMOTE_INCIDENT = { "id": 1, "created": (datetime.now() - timedelta(minutes=10)).strftime(XSOAR_DATE_FORMAT), "CustomFields": {"custom_field": "some_custom_field"}, } def test_fetch_incidents(mocker): """ Given: - List of incidents. When: - Running the fetch_incidents and getting these incidents. Then: - Ensure 1. The incidents result and the last_fetch in the LastRun object as expected. 2. The integration context is updated as expected. """ mocker.patch.object(Client, "search_incidents", return_value=INCIDENTS) mock_integration_context = mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") first_fetch = dateparser.parse("3 days").strftime(XSOAR_DATE_FORMAT) client = Client("") next_run, incidents_result = fetch_incidents( client=client, max_results=3, last_run={}, last_fetch=first_fetch, first_fetch_time=first_fetch, query="", mirror_direction="None", mirror_tag=[], fetch_incident_history=True, ) assert len(incidents_result) == 3 assert dateparser.parse(next_run["last_fetch"]) == dateparser.parse(INCIDENTS[-1]["created"]) assert mock_integration_context.call_args.kwargs["context"] == {"XSOARMirror_mirror_reset": {1: True, 2: True, 3: True}} def test_fetch_incidents_with_integration_context(mocker): """ Given: - List of incidents + List of incident IDs in context (from previous fetch). When: - Running the fetch_incidents and getting these incidents. Then: - Ensure 1. The incidents result and the last_fetch in the LastRun object as expected. 2. The integration context is updated as expected. """ mocker.patch.object(Client, "search_incidents", return_value=INCIDENTS) mocker.patch("XSOARmirroring.get_integration_context", return_value=INCIDENTS_IN_CONTEXT) mock_integration_context = mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") first_fetch = dateparser.parse("3 days").strftime(XSOAR_DATE_FORMAT) client = Client("") next_run, incidents_result = fetch_incidents( client=client, max_results=3, last_run={}, last_fetch=first_fetch, first_fetch_time=first_fetch, query="", mirror_direction="None", mirror_tag=[], fetch_incident_history=True, ) assert len(incidents_result) == 3 assert dateparser.parse(next_run["last_fetch"]) == dateparser.parse(INCIDENTS[-1]["created"]) assert mock_integration_context.call_args.kwargs["context"] == { "XSOARMirror_mirror_reset": { 4: True, 5: True, 6: True, 1: True, 2: True, 3: True, } } @pytest.mark.parametrize("mirror_playbook_id", (True, False)) def test_fetch_incidents_mirror_playbook_id(mocker, mirror_playbook_id: bool): """ Given: - a list of incidents. When: - Running the fetch_incidents and getting this incident, with the *implicit* default `mirror_playbook_id = True`. Then: - Ensure the incident result does not contain playbookId field if and only if `mirror_playbook_id` is False. """ mocker.patch.object(Client, "search_incidents", side_effect=[INCIDENTS_MIRRORING_PLAYBOOK_ID, []]) first_fetch = dateparser.parse("3 days").strftime(XSOAR_DATE_FORMAT) client = Client("dummy token") next_run, incidents_result = fetch_incidents( client=client, max_results=3, last_run={}, first_fetch_time=first_fetch, last_fetch="", query="", mirror_direction="None", mirror_tag=[], mirror_playbook_id=mirror_playbook_id, ) assert len(incidents_result) == 1 assert ("playbookId" in incidents_result[0]) is mirror_playbook_id def test_update_remote_system(mocker): """ Given: - Old incident and fields that were changed. When: - Running the update_remote_system_command. Then: - Ensure the incident was updated. """ args = {"incidentChanged": True, "remoteId": 1, "delta": {"custom_field": "updated_field"}} client = generate_dummy_client() mocker.patch.object(client, "get_incident", return_value=REMOTE_INCIDENT) result = mocker.patch.object(client, "update_incident") update_remote_system_command(client, args, {}) assert result.call_args.kwargs["incident"]["CustomFields"]["custom_field"] == args["delta"]["custom_field"] @pytest.mark.parametrize( "params, expected_url", [ ( {"credentials_api_key": {"identifier": "key_id", "password": "test_password"}, "url": "https://my-example.com"}, "https://my-example.com/xsoar", ), ( {"credentials_api_key": {"identifier": "key_id", "password": "test_password"}, "url": "https://my-example.com/xsoar"}, "https://my-example.com/xsoar", ), ( {"credentials_api_key": {"identifier": "", "password": "test_password"}, "url": "https://my-example.com"}, "https://my-example.com", ), ({"credentials_api_key": {"identifier": ""}, "url": "https://my-example.com"}, "https://my-example.com"), ], ) def test_validate_and_prepare_basic_params(params, expected_url): """ Given: Case a: parameters with API Key ID (key_id) and a URL not containing the 'xsoar' suffix. Case b: parameters with API Key ID (key_id) and a URL containing the 'xsoar' suffix. Case c: parameters with no API Key ID (key_id) and a URL not containing the 'xsoar' suffix. Case c: parameters with no API Key. Whe: Validating and preparing the basic params of api_key_id, api_key, base_url Then: Case a: Make sure the base url receives the 'xsoar' suffix Case b: Make sure the base url keeps the 'xsoar' suffix Case c: Make sure the base url does not receive the 'xsoar' suffix Case d: An exception is thrown with message of: 'API Key must be provided' """ if not params.get("credentials_api_key").get("password"): with pytest.raises(DemistoException) as e: validate_and_prepare_basic_params(params) assert e.message == "API Key must be provided." else: _, _, full_base_url = validate_and_prepare_basic_params(params) assert full_base_url == expected_url # test_dedup_incidents parametrize arguments case_incidents_with_different_times = ( "2023-09-26T15:13:45.000000Z", # responses from search_incidents [ [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:14:45Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:15:45Z"}, {"id": "4", "version": 8, "created": "2023-09-26T15:16:45Z"}, {"id": "5", "version": 8, "created": "2023-09-26T15:17:45Z"}, ], [], ], # max fetch 5, # incidents_last_fetch_ids [], ( # expected incident result [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:14:45Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:15:45Z"}, {"id": "4", "version": 8, "created": "2023-09-26T15:16:45Z"}, {"id": "5", "version": 8, "created": "2023-09-26T15:17:45Z"}, ], # expected incidents_last_fetch_ids result ["5"], dateparser.parse("2023-09-26T15:17:45Z"), ), ) case_incidents_with_the_same_times = ( "2023-09-26T15:13:45.000000Z", # responses from search_incidents [ [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "4", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "5", "version": 8, "created": "2023-09-26T15:13:45Z"}, ], [], ], # max fetch 5, # incidents_last_fetch_ids [], ( # expected incident result [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "4", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "5", "version": 8, "created": "2023-09-26T15:13:45Z"}, ], # expected incidents_last_fetch_ids result ["1", "2", "3", "4", "5"], dateparser.parse("2023-09-26T15:13:45Z"), ), ) case_with_empty_response_with_incidents_last_fetch_ids = ( "2023-09-26T15:13:41.000000Z", # responses from search_incidents [[], []], # max fetch 5, # incidents_last_fetch_ids ["1", "2", "3", "4", "5"], ( # expected incident result [], # expected incidents_last_fetch_ids result ["1", "2", "3", "4", "5"], dateparser.parse("2023-09-26T15:13:41Z"), ), ) case_with_empty_response_without_incidents_last_fetch_ids = ( "2023-09-26T15:13:41.000000Z", # responses from search_incidents [[], []], # max fetch 5, # incidents_last_fetch_ids [], ( # expected incident result [], # expected incidents_last_fetch_ids result [], dateparser.parse("2023-09-26T15:13:41Z"), ), ) case_with_more_then_one_API_call_with_incidents_last_fetch_ids = ( "2023-09-26T15:13:41.000000Z", # responses from search_incidents [ [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:41Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:13:42Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:43Z"}, {"id": "4", "version": 8, "created": "2023-09-26T15:13:44Z"}, {"id": "5", "version": 8, "created": "2023-09-26T15:13:45Z"}, ], [ {"id": "5", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "6", "version": 8, "created": "2023-09-26T15:13:46Z"}, {"id": "7", "version": 8, "created": "2023-09-26T15:13:47Z"}, {"id": "8", "version": 8, "created": "2023-09-26T15:13:48Z"}, {"id": "9", "version": 8, "created": "2023-09-26T15:13:49Z"}, ], ], 5, # max fetch ["1"], # incidents_last_fetch_ids ( # expected incident result [ {"id": "2", "version": 8, "created": "2023-09-26T15:13:42Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:43Z"}, {"id": "4", "version": 8, "created": "2023-09-26T15:13:44Z"}, {"id": "5", "version": 8, "created": "2023-09-26T15:13:45Z"}, {"id": "6", "version": 8, "created": "2023-09-26T15:13:46Z"}, ], # expected incidents_last_fetch_ids result ["6"], dateparser.parse("2023-09-26T15:13:46Z"), ), ) case_with_an_incident_that_was_fetched = ( "2023-09-26T15:13:41.000000Z", # responses from search_incidents [ [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:41Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:13:42Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:43Z"}, ], [], ], 5, # max fetch ["1"], # incidents_last_fetch_ids ( # expected incident result [ {"id": "2", "version": 8, "created": "2023-09-26T15:13:42Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:43Z"}, ], # expected incidents_last_fetch_ids result ["3"], dateparser.parse("2023-09-26T15:13:43Z"), ), ) case_with_an_incident_that_was_fetched_and_there_are_more_with_the_same_time = ( "2023-09-26T15:13:41.000000Z", # responses from search_incidents [ [ {"id": "1", "version": 8, "created": "2023-09-26T15:13:41Z"}, {"id": "2", "version": 8, "created": "2023-09-26T15:13:41Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:41Z"}, ], [], dateparser.parse("2023-09-26T15:13:41Z"), ], 5, # max fetch ["1"], # incidents_last_fetch_ids ( # expected incident result [ {"id": "2", "version": 8, "created": "2023-09-26T15:13:41Z"}, {"id": "3", "version": 8, "created": "2023-09-26T15:13:41Z"}, ], # expected incidents_last_fetch_ids result ["1", "2", "3"], dateparser.parse("2023-09-26T15:13:41Z"), ), ) case_incidents_not_utc_time = ( "2023-11-09T03:25:05.000000Z", # responses from search_incidents [ [ {"id": "1", "version": 8, "created": "2023-11-09T06:25:06.828698605+03:00"}, {"id": "2", "version": 8, "created": "2023-11-09T06:26:06.828698605+03:00"}, ], [], ], # max fetch 5, # incidents_last_fetch_ids [], ( # expected incident result [ {"id": "1", "version": 8, "created": "2023-11-09T06:25:06.828698605+03:00"}, {"id": "2", "version": 8, "created": "2023-11-09T06:26:06.828698605+03:00"}, ], # expected incidents_last_fetch_ids result ["2"], dateparser.parse("2023-11-09T03:26:06.828698605Z"), ), ) @pytest.mark.parametrize( "last_fetch, incident_to_return , max_fetch, incidents_last_fetch_ids, expected_result", [ case_incidents_with_different_times, case_incidents_with_the_same_times, case_with_empty_response_with_incidents_last_fetch_ids, case_with_empty_response_without_incidents_last_fetch_ids, case_with_more_then_one_API_call_with_incidents_last_fetch_ids, case_with_an_incident_that_was_fetched, case_with_an_incident_that_was_fetched_and_there_are_more_with_the_same_time, case_incidents_not_utc_time, ], ) def test_dedup_incidents_with_seconds_timestamp( mocker, last_fetch, incident_to_return, max_fetch, incidents_last_fetch_ids, expected_result, ): """ Given: - Case 1: All incidents from the current fetch cycle have different timestamp. - Case 2: All incidents from the current fetch cycle have the same timestamp and were not fetched. - Case 3: All incidents from the previous fetch cycle were fetched. No new incidents received from API response. - Case 4: Empty response without incidents_last_fetch_ids provided. - Case 5: More than one API call received with incidents_last_fetch_ids provided. - Case 6: An incident that was already fetched in the previous run is received again. - Case 7: Incidents with equal time stamp to an incident that was already fetched were received. When: - Using the dedup mechanism while fetching incidents. Then: - Verify that the dedup mechanism correctly handles the different test cases by comparing the expected and actual results. """ from XSOARmirroring import get_and_dedup_incidents client = Client("") mocker.patch.object(Client, "search_incidents", side_effect=incident_to_return) assert get_and_dedup_incidents(client, incidents_last_fetch_ids, "", max_fetch, last_fetch) == expected_result def test_get_incident_entries_without_entries(mocker): """ Given: - incident_id and date. When: - Running the get_incident_entries request. Then: - Ensure that an empty list is returned when there is no entries. """ from XSOARmirroring import Client client = Client(base_url="https://test.com") mocker.patch.object( client, "_http_request", return_value={ "closed": "2023-09-20T10:54:00.669862412Z", "closingUserId": "DBot", "created": "2023-09-20T09:07:46.457488661Z", "details": "", }, ) result = client.get_incident_entries( incident_id="1", from_date="1696494896", max_results=1, categories=None, tags_and_operator=True, tags=None, ) assert result is not None assert result == [] # ── get-modified-remote-data tests ──────────────────────────────────────────── def test_get_modified_remote_data_returns_ids(mocker): """ Given: - A lastUpdate timestamp and a remote server that returns two modified incident IDs. - No unsupported-endpoint flag in integration context. When: - Running get_modified_remote_data_command. Then: - The response contains exactly those two incident IDs. - The stale flag is NOT written (no failure occurred). """ last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} mocker.patch("XSOARmirroring.get_integration_context", return_value={}) mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mocker.patch.object( Client, "get_modified_incidents", return_value=["101", "202"], ) client = Client(base_url="https://test.com") result = get_modified_remote_data_command(client, args) assert result.modified_incident_ids == ["101", "202"] def test_get_modified_remote_data_empty_response(mocker): """ Given: - A lastUpdate timestamp and a remote server that returns no modified incidents. - No unsupported-endpoint flag in integration context. When: - Running get_modified_remote_data_command. Then: - The response contains an empty list of incident IDs. """ last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} mocker.patch("XSOARmirroring.get_integration_context", return_value={}) mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mocker.patch.object( Client, "get_modified_incidents", return_value=[], ) client = Client(base_url="https://test.com") result = get_modified_remote_data_command(client, args) assert result.modified_incident_ids == [] def test_get_modified_remote_data_passes_correct_timestamp(mocker): """ Given: - A specific lastUpdate ISO8601 timestamp. - No unsupported-endpoint flag in integration context. When: - Running get_modified_remote_data_command. Then: - The client's get_modified_incidents is called with the correct epoch seconds derived from lastUpdate. """ last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) expected_epoch = int(last_update.timestamp()) args = {"lastUpdate": last_update.isoformat()} mocker.patch("XSOARmirroring.get_integration_context", return_value={}) mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mock_get_modified = mocker.patch.object( Client, "get_modified_incidents", return_value=["999"], ) client = Client(base_url="https://test.com") get_modified_remote_data_command(client, args) mock_get_modified.assert_called_once_with(from_timestamp=expected_epoch) def test_get_modified_incidents_client_method(mocker): """ Given: - A mock HTTP response from /incidents/modified returning a list of incident IDs. When: - Calling client.get_modified_incidents with a from_timestamp. Then: - The method returns the list of incident IDs as-is. """ client = Client(base_url="https://test.com") mocker.patch.object( client, "_http_request", return_value=["101", "202", "303"], ) result = client.get_modified_incidents(from_timestamp=1740830400) assert sorted(result) == ["101", "202", "303"] def test_get_modified_incidents_client_method_empty_response(mocker): """ Given: - A mock HTTP response from /incidents/modified returning an empty dict. When: - Calling client.get_modified_incidents. Then: - The method returns an empty list. """ client = Client(base_url="https://test.com") mocker.patch.object( client, "_http_request", return_value={}, ) result = client.get_modified_incidents(from_timestamp=1740830400) assert result == [] @pytest.mark.parametrize("status_code", [303, 404]) def test_get_modified_remote_data_unsupported_endpoint_handled_gracefully(mocker, status_code): """ Given: - client.get_modified_incidents raises a DemistoException with a 303 or 404 response, indicating the remote machine does not support the /public/v1/incidents/modified endpoint. - No unsupported-endpoint flag is set in integration context yet. When: - Running get_modified_remote_data_command. Then: - demisto.error is called twice: once for the general log and once for the user-facing message. - The user-facing error message mentions the unsupported endpoint and actionable guidance. - demisto.results is called with an ERROR entry whose Contents explains the issue. - Today's UTC date is persisted to integration context under MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY. - sys.exit(0) is called to terminate gracefully. """ import sys from unittest.mock import MagicMock last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} mock_res = MagicMock() mock_res.status_code = status_code exc = DemistoException("Endpoint not found", res=mock_res) integration_context: dict = {} mocker.patch("XSOARmirroring.get_integration_context", return_value=integration_context) mock_set_context = mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mocker.patch.object(Client, "get_modified_incidents", side_effect=exc) mock_error = mocker.patch("demistomock.error") mock_results = mocker.patch("demistomock.results") mock_exit = mocker.patch.object(sys, "exit", side_effect=SystemExit) client = Client(base_url="https://test.com") with pytest.raises(SystemExit): get_modified_remote_data_command(client, args) # demisto.error should be called twice: general log + user-facing message assert mock_error.call_count == 2 all_error_msgs = " ".join(call[0][0] for call in mock_error.call_args_list) assert "/public/v1/incidents/modified" in all_error_msgs assert str(status_code) in all_error_msgs # demisto.results should carry an ERROR entry with the actionable message mock_results.assert_called_once() result_entry = mock_results.call_args[0][0] assert result_entry["Type"] == EntryType.ERROR # 4 contents = result_entry["Contents"] assert "/public/v1/incidents/modified" in contents assert "XSOAR 8" in contents mock_exit.assert_called_once_with(0) # The unsupported-endpoint flag must have been persisted to integration context today_utc = datetime.now(tz=UTC).strftime("%Y-%m-%d") assert integration_context.get(MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY) == today_utc mock_set_context.assert_called_once() @pytest.mark.parametrize("status_code", [303, 404]) def test_get_modified_remote_data_circuit_breaker_skips_call_when_flag_set(mocker, status_code): """ Given: - The integration context already contains today's UTC date under MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY, meaning the endpoint was found unsupported earlier today. When: - Running get_modified_remote_data_command. Then: - client.get_modified_incidents is NOT called (no HTTP request is made). - demisto.results is called with an ERROR entry containing the cached error message. - sys.exit(0) is called to terminate gracefully. """ import sys last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} today_utc = datetime.now(tz=UTC).strftime("%Y-%m-%d") mocker.patch( "XSOARmirroring.get_integration_context", return_value={MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY: today_utc}, ) mock_get_modified = mocker.patch.object(Client, "get_modified_incidents") mock_results = mocker.patch("demistomock.results") mocker.patch("demistomock.debug") mocker.patch("demistomock.error") mock_exit = mocker.patch.object(sys, "exit", side_effect=SystemExit) client = Client(base_url="https://test.com") with pytest.raises(SystemExit): get_modified_remote_data_command(client, args) # No HTTP call should have been made mock_get_modified.assert_not_called() # An ERROR entry must be returned with the cached message mock_results.assert_called_once() result_entry = mock_results.call_args[0][0] assert result_entry["Type"] == EntryType.ERROR assert "/public/v1/incidents/modified" in result_entry["Contents"] mock_exit.assert_called_once_with(0) def test_get_modified_remote_data_circuit_breaker_retries_next_day(mocker): """ Given: - The integration context contains yesterday's UTC date under MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY. When: - Running get_modified_remote_data_command. Then: - client.get_modified_incidents IS called (the flag is stale — different day). - The response contains the returned incident IDs. """ from datetime import timedelta last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} yesterday_utc = (datetime.now(tz=UTC) - timedelta(days=1)).strftime("%Y-%m-%d") mocker.patch( "XSOARmirroring.get_integration_context", return_value={MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY: yesterday_utc}, ) mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mock_get_modified = mocker.patch.object( Client, "get_modified_incidents", return_value=["42"], ) client = Client(base_url="https://test.com") result = get_modified_remote_data_command(client, args) mock_get_modified.assert_called_once() assert result.modified_incident_ids == ["42"] def test_get_modified_remote_data_clears_flag_on_success(mocker): """ Given: - The integration context contains a stale unsupported-endpoint flag (from a previous day). - The remote server now responds successfully. When: - Running get_modified_remote_data_command. Then: - The MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY key is removed from integration context. - set_to_integration_context_with_retries is called to persist the cleared context. - The response contains the returned incident IDs. """ from datetime import timedelta last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} yesterday_utc = (datetime.now(tz=UTC) - timedelta(days=1)).strftime("%Y-%m-%d") integration_context = {MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY: yesterday_utc} mocker.patch("XSOARmirroring.get_integration_context", return_value=integration_context) mock_set_context = mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mocker.patch.object(Client, "get_modified_incidents", return_value=["77"]) client = Client(base_url="https://test.com") result = get_modified_remote_data_command(client, args) # Flag must have been removed assert MODIFIED_ENDPOINT_UNSUPPORTED_DATE_KEY not in integration_context mock_set_context.assert_called_once() assert result.modified_incident_ids == ["77"] def test_get_modified_remote_data_no_flag_cleared_when_context_clean(mocker): """ Given: - The integration context has no unsupported-endpoint flag. - The remote server responds successfully. When: - Running get_modified_remote_data_command. Then: - set_to_integration_context_with_retries is NOT called (nothing to clear). - The response contains the returned incident IDs. """ last_update = datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC) args = {"lastUpdate": last_update.isoformat()} mocker.patch("XSOARmirroring.get_integration_context", return_value={}) mock_set_context = mocker.patch("XSOARmirroring.set_to_integration_context_with_retries") mocker.patch.object(Client, "get_modified_incidents", return_value=["55"]) client = Client(base_url="https://test.com") result = get_modified_remote_data_command(client, args) mock_set_context.assert_not_called() assert result.modified_incident_ids == ["55"]