Cortex XDR - IOC
Use the Cortex XDR - IOCs feed integration to sync indicators from Cortex XSOAR to Cortex XDR and back to Cortex XSOAR. Cortex XDR is the world's first detection and response app that natively integrates network, endpoint and cloud data to stop sophisticated attacks.
Data Enrichment & Threat Intelligence · Cortex XDR by Palo Alto Networks · Feed
Details
| ID | Cortex XDR - IOC |
|---|---|
| Provider | Palo Alto Networks |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 5.5.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix |
README
Cortex XDR is the world’s first detection and response app that natively integrates network, endpoint and cloud data to stop sophisticated attacks.
Use the Cortex XDR - IOCs feed integration to sync indicators between Cortex XSOAR and Cortex XDR. The integration will sync indicators according to the defined fetch interval. At each interval, the integration will push new and modified indicators defined in the Sync Query from Cortex XSOAR to Cortex XDR. Additionally, the integration will check if there are manual modifications of indicators on Cortex XDR and sync back to Cortex XSOAR. Once per day, the integration will perform a complete sync which will also remove indicators that have been deleted/expired in Cortex XSOAR, from Cortex XDR.
This integration was integrated and tested with Branch: stable-50 of XDR.
Prerequisites
An API key of type Advanced with an Administrator role.
Configure Cortex XDR - IOC in Cortex
| Parameter | Description | Required |
|---|---|---|
| url | Server URL (e.g. https://example.net) | True |
| apikey_id | API Key ID | True |
| apikey | API Key | True |
| feed | Fetch indicators | False |
| severity | The severity in Cortex XDR | True |
| Tags | Appears in Cortex XSOAR if a modification was made on the Cortex XDR side and is being “mirrored” to Cortex XSOAR | False |
| query | Sync Query | True |
| insecure | Trust any certificate (not secure) | False |
| xsoar_severity_field | The Cortex XSOAR indicator field used as severity. | True |
| xsoar_comments_field | The Cortex XSOAR field where comments are stored. Default is comments. Expecting an XSOAR IOC format of a comment (nested dictionary). See Comments As Tags for more. |
True |
| comments_as_tags | Whether to consider the value at xsoar_comments_field as CSV. Requires specifying a xsoar_comments_field value different than the default comments. |
True |
| proxy | Use system proxy settings | False |
| feedReputation | Indicator Reputation | False |
| feedReliability | Source Reliability | True |
| tlp_color | The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed. More information about the protocol can be found at https://us-cert.cisa.gov/tlp | False |
| feedExpirationPolicy | False | |
| feedExpirationInterval | False | |
| feedFetchInterval | Feed Fetch Interval (make sure to set it to at least 15 minutes) | False |
| feedBypassExclusionList | Bypass exclusion list | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
xdr-iocs-sync
Sync IOCs with Cortex XDR.
Run this command manually only when configuring the instance integration with fetch indicators disabled (run this only once).
It is not recommended to run this manually when there are more then 40,000 indicators.
When fetch indicators is enabled, the sync mechanism is used by default. This sets the current time as the last sync time and fetches IOCs from Cortex XSOAR to Cortex XDR, sorted by modification time, in batches of 40,000, up to that time. Upon reaching the last sync point, the synchronization becomes bi-directional, first from Cortex XSOAR to Cortex XDR, then from Cortex XDR to Cortex XSOAR.
As a result, the duration of the first sync depends on the number of IOCs in the Cortex XSOAR tenant and the Feed Fetch Interval. For example, if there are 800,000 indicators in Cortex XSOAR and the Feed Fetch Interval is set to 20 minutes as recommended, the initial sync process will take approximately 7 hours.
Base Command
xdr-iocs-sync
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Command Example
!xdr-iocs-sync
Human Readable Output
sync with XDR completed.
xdr-iocs-push
Push new or modified IOCs to Cortex XDR.
Base Command
xdr-iocs-push
Input
| Argument Name | Description | Required |
|---|---|---|
| indicator | the indicators | Optional |
Context Output
There is no context output for this command.
Command Example
xdr-iocs-push
Human Readable Output
push success.
xdr-iocs-enable
Enable iocs in XDR server
Base Command
xdr-iocs-enable
Input
| Argument Name | Description | Required |
|---|---|---|
| indicator | The indicator to enable | Required |
Context Output
There is no context output for this command.
Command Example
!xdr-iocs-enable indicator=11.11.11.11
Human Readable Output
indicators 11.11.11.11 enabled.
xdr-iocs-disable
Disable iocs in XDR server
Base Command
xdr-iocs-disable
Input
| Argument Name | Description | Required |
|---|---|---|
| indicator | The indicator to enable | Required |
Context Output
There is no context output for this command.
Command Example
!xdr-iocs-disable indicator=22.22.22.22
Human Readable Output
indicators 22.22.22.22 disabled.
xdr-iocs-set-sync-time
Set sync time manually (Do not use this command unless you unredstandard the consequences).
Base Command
xdr-iocs-set-sync-time
Input
| Argument Name | Description | Required |
|---|---|---|
| time | The time of the file creation (use UTC time zone). | Required |
Context Output
There is no context output for this command.
xdr-iocs-create-sync-file
Creates the sync file for the manual process. Run this command when instructed by the XDR support team.
Base Command
xdr-iocs-create-sync-file
Input
| Argument Name | Description | Required |
|---|---|---|
| zip | Whether to zip the output file. | Required |
| set_time | Whether to modify the sync time locally. | Required |
Context Output
There is no context output for this command.
Base Command
xdr-iocs-to-keep-file
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (e.g. https://example.net) (required)apikey_id_creds—apikey_creds—apikey_id— API Key IDapikey— API Keyfeed— Fetch indicatorsautoSync— Auto Syncseverity— Overriding severity value (required)feedTags— Tagsquery— Sync Query (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsfeedReputation— Indicator ReputationfeedReliability— Source Reliability (required)tlp_color— Traffic Light Protocol ColorfeedExpirationPolicy—feedExpirationInterval—feedIncremental— Incremental FeedfeedFetchInterval— Feed Fetch Intervalxsoar_severity_field— XSOAR Severity Fieldxsoar_comments_field— XSOAR Comment Field Exporting To XDRcomments_as_tags— Comments as tags (CSV)override_severity— Override severityfeedBypassExclusionList— Bypass exclusion listextensive_logs— Extensive logging (for debugging purposes)
Commands (7)
-
xdr-iocs-create-sync-fileCreates the sync file for the manual process. Run this command when instructed by the XDR support team.
-
xdr-iocs-disableDisables IOCs in the XDR server.
-
xdr-iocs-enableEnables IOCs in the XDR server.
-
xdr-iocs-pushPush modified IOCs to Cortex XDR.
-
xdr-iocs-set-sync-timeDeprecatedSet sync time manually. (Do not use this command unless you understand the consequences.)
-
xdr-iocs-syncSync your IOC with Cortex XDR.
-
xdr-iocs-to-keep-fileCreate a file with all the IOCs that are going to sync to Cortex XDR.
import tempfile from unittest.mock import MagicMock, patch import pytest from freezegun import freeze_time from XDR_iocs import * Client.severity = "INFO" client = Client({"url": "https://example.com"}) def d_sort(in_dict): return sorted(in_dict.items()) class TestGetHeaders: @freeze_time("2020-06-01T00:00:00Z") def test_sanity(self, mocker): """ Given: - API key - API key ID Then: - Verify headers created correct. """ params = { "apikey_id": "7", "apikey": "aaaaaaa", # noqa: E501 } headers = { "Authorization": "e9a63fb06148bd3a73ce93c8b44c083a147cafb0fe607e706abcac25759b3c43", # it's a dummy Authorization "x-iocs-source": "xsoar", "x-xdr-auth-id": "7", "x-xdr-nonce": "1111111111111111111111111111111111111111111111111111111111111111", "x-xdr-timestamp": "1590969600000", } mocker.patch("secrets.choice", return_value="1") output = get_headers(params) assert output == headers, f"get_headers({params})\n\treturns: {d_sort(output)}\n\tinstead: {d_sort(headers)}" def test_empty_case(self): """ Given: Empty params Then: get_headers will not raise error """ get_headers({}) class TestHttpRequest: def test_http_request_ok(self, requests_mock): """ Given: - a client When: - http_request returns status code 200. Then: - do not raise an error """ requests_mock.post("https://example.com/public_api/v1/indicators/suffix", status_code=200, json={}) client.http_request(url_suffix="suffix", requests_kwargs={}) @pytest.mark.parametrize("status_code", client.error_codes.keys()) def test_http_request_error(self, requests_mock, status_code): """ Given: - Status code When: - http_request returns this status code. Then: - Verify error message. - Verify exception.res status code matches the http status code. """ with pytest.raises(DemistoException) as e: requests_mock.post("https://example.com/public_api/v1/indicators/suffix", status_code=status_code) client.http_request("suffix", requests_kwargs={}) assert e.value.message == client.error_codes[status_code] assert e.value.res.status_code == status_code def test_http_request_bad_json(self, requests_mock): # For an unknown reason, this does not pass locally, but only on the CI. """ Given: - a client When: - http_request returns a response that is not a json. Then: - Verify error message. - Verify demisto exception """ text = "not a json" with pytest.raises(DemistoException) as e: requests_mock.post("https://example.com/public_api/v1/indicators/suffix", status_code=200, text=text) client.http_request("suffix", requests_kwargs={}) assert e.value.message == f"Could not parse json out of {text}" assert e.value.res.status_code == 200 assert isinstance(e.value.exception, requests.exceptions.JSONDecodeError | json.decoder.JSONDecodeError) class TestGetRequestsKwargs: def test_with_file(self, mocker): """ Given: - file to upload Then: - Verify output format. """ def override_open(open_path, *_other): return open_path mocker.patch("builtins.open", side_effect=override_open) path = "/Users/some_user/some_dir/some_file.file" output = get_requests_kwargs(file_path=path) expected_output = {"files": [("file", ("iocs.json", path, "application/json"))]} assert ( output == expected_output ), f"get_requests_kwargs(file_path={path})\n\treturns: {output}\n\t instead: {expected_output}" # noqa: E501 def test_with_json(self): """ Given: - simple json Then: - the json ready to send """ _json = {"test": "test"} output = get_requests_kwargs(_json=_json) expected_output = {"data": '{"request_data": {"test": "test"}}'} assert ( output == expected_output ), f"get_requests_kwargs(_json={_json})\n\treturns: {output}\n\t instead: {expected_output}" # noqa: E501 class TestPrepareCommands: @freeze_time("2022-04-14T00:00:00Z") def test_prepare_get_changes(self): """ Given: - get changes command Then: - Verify url and json format. """ ts = int(datetime.now(timezone.utc).timestamp() * 1000) url_suffix, _json = prepare_get_changes(ts) assert ( url_suffix == "get_changes" ), f"prepare_get_changes\n\treturns url_suffix: {url_suffix}\n\tinstead url_suffix: get_changes" # noqa: E501 assert _json == {"last_update_ts": ts} def test_prepare_enable_iocs(self): """ Given: - enable iocs command Then: - Verify url and json format. """ url_suffix, iocs = prepare_enable_iocs("8.8.8.8,domain.com") assert ( url_suffix == "enable_iocs" ), f"prepare_enable_iocs\n\treturns url_suffix: {url_suffix}\n\tinstead url_suffix: enable_iocs" # noqa: E501 assert iocs == ["8.8.8.8", "domain.com"] def test_prepare_disable_iocs(self): """ Given: - disable iocs command Then: - Verify url and json format. """ url_suffix, iocs = prepare_disable_iocs("8.8.8.8,domain.com") assert ( url_suffix == "disable_iocs" ), f"prepare_disable_iocs\n\treturns url_suffix: {url_suffix}\n\tinstead url_suffix: disable_iocs" # noqa: E501 assert iocs == ["8.8.8.8", "domain.com"] class TestCreateFile: path = "test_data/sync_file_test.json" data_test_create_file_sync = [ ("Domain_iocs", "Domain_sync_file"), ("IP_iocs", "IP_sync_file"), ("File_iocs", "File_sync_file"), ] data_test_create_file_iocs_to_keep = [ ("Domain_iocs", "Domain_iocs_to_keep_file"), ("IP_iocs", "IP_iocs_to_keep_file"), ("File_iocs", "File_iocs_to_keep_file"), ] @staticmethod def get_file(path): with open(path) as _file: return _file.read() @staticmethod def get_all_iocs(go_over, extension): iocs = [] total = 0 data = [] for in_iocs, out_iocs in go_over: ioc = json.loads(TestCreateFile.get_file(f"test_data/{in_iocs}.json")) iocs.extend(ioc["iocs"]) total += ioc["total"] data.append(TestCreateFile.get_file(f"test_data/{out_iocs}.{extension}")) all_iocs = {"iocs": iocs, "total": total} all_data = "".join(data) return all_iocs, all_data def test_create_file_sync_without_iocs(self, mocker): """ Given: - Sync command When: - there is no iocs Then: - Verify sync file data. """ mocker.patch.object(demisto, "searchIndicators", return_value={"total": 0}) with tempfile.NamedTemporaryFile(mode="w") as temp_file: create_file_sync(temp_file.name) data = self.get_file(temp_file.name) expected_data = "" assert data == expected_data, f"create_file_sync with no iocs\n\tcreates: {data}\n\tinstead: {expected_data}" @pytest.mark.parametrize("in_iocs, out_iocs", data_test_create_file_sync) def test_create_file_sync(self, in_iocs, out_iocs, mocker): """ Given: - Sync command When: - iocs type is a specific type. Then: - Verify sync file data. """ mocker.patch.object(demisto, "searchIndicators", return_value=json.loads(self.get_file(f"test_data/{in_iocs}.json"))) # noqa: E501 with tempfile.NamedTemporaryFile(mode="w") as temp_file: create_file_sync(temp_file.name) data = self.get_file(temp_file.name) expected_data = self.get_file(f"test_data/{out_iocs}.txt") assert data == expected_data, f"create_file_sync with {in_iocs} iocs\n\tcreates: {data}\n\tinstead: {expected_data}" def test_create_file_sync_all_types(self, mocker): """ Given: - Sync command When: - iocs as all types Then: - Verify sync file data. """ all_iocs, expected_data = self.get_all_iocs(self.data_test_create_file_sync, "txt") mocker.patch.object(demisto, "searchIndicators", return_value=all_iocs) with tempfile.NamedTemporaryFile(mode="w") as temp_file: create_file_sync(temp_file.name) data = self.get_file(temp_file.name) assert data == expected_data, f"create_file_sync with all iocs\n\tcreates: {data}\n\tinstead: {expected_data}" data_test_create_file_with_empty_indicators = [{}, {"value": "11.11.11.11"}, {"indicator_type": "IP"}] @pytest.mark.parametrize("defective_indicator", data_test_create_file_with_empty_indicators) def test_create_file_sync_with_empty_indicators(self, defective_indicator, mocker): """ Given: - Sync command When: - a part iocs dont have all required data Then: - Verify sync file data. """ all_iocs, expected_data = self.get_all_iocs(self.data_test_create_file_sync, "txt") all_iocs["iocs"].append(defective_indicator) all_iocs["total"] += 1 mocker.patch.object(demisto, "searchIndicators", return_value=all_iocs) with tempfile.NamedTemporaryFile(mode="w") as temp_file: create_file_sync(temp_file.name) data = self.get_file(temp_file.name) assert data == expected_data, f"create_file_sync with all iocs\n\tcreates: {data}\n\tinstead: {expected_data}" def test_create_file_iocs_to_keep_without_iocs(self, mocker): """ Given: - iocs to keep command When: - there is no iocs Then: - Verify iocs to keep file data. """ mocker.patch.object(demisto, "searchIndicators", return_value={"total": 0}) with tempfile.NamedTemporaryFile(mode="w") as temp_file: create_file_iocs_to_keep(temp_file.name) data = self.get_file(temp_file.name) expected_data = " " assert data == expected_data, f"create_file_iocs_to_keep with no iocs\n\tcreates: {data}\n\tinstead: {expected_data}" @pytest.mark.parametrize("in_iocs, out_iocs", data_test_create_file_iocs_to_keep) def test_create_file_iocs_to_keep(self, in_iocs, out_iocs, mocker): """ Given: - iocs to keep command When: - iocs type is a specific type. Then: - Verify iocs to keep file data. """ mocker.patch.object(demisto, "searchIndicators", return_value=json.loads(self.get_file(f"test_data/{in_iocs}.json"))) with tempfile.NamedTemporaryFile(mode="w") as temp_file: create_file_iocs_to_keep(temp_file.name) data = self.get_file(temp_file.name) expected_data = self.get_file(f"test_data/{out_iocs}.txt") assert ( data == expected_data ), f"create_file_iocs_to_keep with {in_iocs} iocs\n\tcreates: {data}\n\tinstead: {expected_data}" # noqa: E501 def test_create_file_iocs_to_keep_all_types(self, mocker): """ Given: - iocs to keep command When: - iocs as all types Then: - Verify iocs to keep file data. """ all_iocs, expected_data = self.get_all_iocs(self.data_test_create_file_iocs_to_keep, "txt") mocker.patch.object(demisto, "searchIndicators", return_value=all_iocs) with tempfile.NamedTemporaryFile("w") as temp_file: create_file_iocs_to_keep(temp_file.name) data = self.get_file(temp_file.name) assert data == expected_data, f"create_file_iocs_to_keep with all iocs\n\tcreates: {data}\n\tinstead: {expected_data}" class TestDemistoIOCToXDR: data_test_demisto_expiration_to_xdr = [ (None, -1), ("", -1), ("0001-01-01T00:00:00Z", -1), ("2020-06-03T00:00:00Z", 1591142400000), ] @pytest.mark.parametrize("demisto_expiration, xdr_expiration", data_test_demisto_expiration_to_xdr) def test_demisto_expiration_to_xdr(self, demisto_expiration, xdr_expiration): """ Given: - demisto indicator expiration Then: - Verify XDR expiration. """ output = demisto_expiration_to_xdr(demisto_expiration) assert ( xdr_expiration == output ), f"demisto_expiration_to_xdr({demisto_expiration})\n\treturns: {output}\n\tinstead: {xdr_expiration}" # noqa: E501 data_test_demisto_reliability_to_xdr = [ (None, "F"), ("A - Completely reliable", "A"), ("B - Usually reliable", "B"), ("C - Fairly reliable", "C"), ("D - Not usually reliable", "D"), ("E - Unreliable", "E"), ("F - Reliability cannot be judged", "F"), ] @pytest.mark.parametrize("demisto_reliability, xdr_reliability", data_test_demisto_reliability_to_xdr) def test_demisto_reliability_to_xdr(self, demisto_reliability, xdr_reliability): """ Given: - demisto indicator reliability Then: - Verify XDR reliability. """ output = demisto_reliability_to_xdr(demisto_reliability) assert ( output == xdr_reliability ), f"demisto_reliability_to_xdr({demisto_reliability})\n\treturns: {output}\n\tinstead: {xdr_reliability}" # noqa: E501 data_test_demisto_types_to_xdr = [("File", "HASH"), ("IP", "IP"), ("Domain", "DOMAIN_NAME"), ("URL", "PATH")] @pytest.mark.parametrize("demisto_type, xdr_type", data_test_demisto_types_to_xdr) def test_demisto_types_to_xdr(self, demisto_type, xdr_type): """ Given: - demisto indicator type Then: - Verify XDR type. """ output = demisto_types_to_xdr(demisto_type) assert output == xdr_type, f"demisto_reliability_to_xdr({demisto_type})\n\treturns: {output}\n\tinstead: {xdr_type}" data_test_demisto_vendors_to_xdr = [ ( {"moduleID": {"sourceBrand": "test", "reliability": "A - Completely reliable", "score": 2}}, {"vendor_name": "test", "reputation": "SUSPICIOUS", "reliability": "A"}, ), ( {"moduleID": {"reliability": "A - Completely reliable", "score": 2}}, {"vendor_name": "moduleID", "reputation": "SUSPICIOUS", "reliability": "A"}, ), ( {"moduleID": {"sourceBrand": "test", "score": 2}}, {"vendor_name": "test", "reputation": "SUSPICIOUS", "reliability": "F"}, ), ( {"moduleID": {"reliability": "A - Completely reliable", "score": 0}}, {"vendor_name": "moduleID", "reputation": "UNKNOWN", "reliability": "A"}, ), ] @pytest.mark.parametrize("demisto_vendor, xdr_vendor", data_test_demisto_vendors_to_xdr) def test_demisto_vendors_to_xdr(self, demisto_vendor, xdr_vendor): """ Given: - demisto indicator vendors reports. Then: - Verify XDR vendors format. """ output = demisto_vendors_to_xdr(demisto_vendor)[0] assert ( output == xdr_vendor ), f"demisto_vendors_to_xdr({demisto_vendor})\n\treturns: {d_sort(output)}\n\tinstead: {d_sort(xdr_vendor)}" # noqa: E501 data_test_demisto_ioc_to_xdr = [ ( {"value": "11.11.11.11", "indicator_type": "IP", "score": 2}, { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "SUSPICIOUS", "severity": "INFO", "type": "IP", "comment": [""], }, ), ( {"value": "11.11.11.11", "indicator_type": 100, "score": 2}, { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "SUSPICIOUS", "severity": "INFO", "type": "100", "comment": [""], }, ), ( {"value": "11.11.11.11", "indicator_type": "IP"}, { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "comment": [""], }, ), ( {"value": "11.11.11.11", "indicator_type": "IP", "expiration": "2020-06-03T00:00:00Z"}, { "expiration_date": 1591142400000, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "comment": [""], }, # noqa: E501 ), ( { "value": "11.11.11.11", "indicator_type": "IP", "comments": [{"type": "IndicatorCommentTimeLine", "content": "test"}], }, # noqa: E501 { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "comment": [""], }, ), ( { "value": "11.11.11.11", "indicator_type": "IP", "comments": [{"type": "IndicatorCommentRegular", "content": "test"}], }, # noqa: E501 { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "comment": ["test"], }, # noqa: E501 ), ( { "value": "11.11.11.11", "indicator_type": "IP", "comments": [ {"type": "IndicatorCommentRegular", "content": "test"}, {"type": "IndicatorCommentRegular", "content": "this is the comment"}, ], }, # noqa: E501 { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "comment": ["this is the comment"], }, # noqa: E501 ), ( {"value": "11.11.11.11", "indicator_type": "IP", "aggregatedReliability": "A - Completely reliable"}, { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "reliability": "A", "comment": [""], }, # noqa: E501 ), ( {"value": "11.11.11.11", "indicator_type": "IP", "CustomFields": {"threattypes": {"threatcategory": "Malware"}}}, # noqa: E501 { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "class": "Malware", "comment": [""], }, # noqa: E501 ), ( {"value": "11.11.11.11", "indicator_type": "IP", "moduleToFeedMap": {"module": {"sourceBrand": "test", "score": 2}}}, # noqa: E501 { "expiration_date": -1, "indicator": "11.11.11.11", "reputation": "UNKNOWN", "severity": "INFO", "type": "IP", "vendors": [{"vendor_name": "test", "reputation": "SUSPICIOUS", "reliability": "F"}], "comment": [""], }, # noqa: E501 ), ] @pytest.mark.parametrize("demisto_ioc, xdr_ioc", data_test_demisto_ioc_to_xdr) def test_demisto_ioc_to_xdr(self, demisto_ioc, xdr_ioc): """ Given: - demisto indicator. Then: - Verify XDR indicator format. """ output = demisto_ioc_to_xdr(demisto_ioc) assert output == xdr_ioc, f"demisto_ioc_to_xdr({demisto_ioc})\n\treturns: {d_sort(output)}\n\tinstead: {d_sort(xdr_ioc)}" # noqa: E501 def test_empty_demisto_ioc_to_xdr(self, mocker): warnings = mocker.patch.object(demisto, "debug") output = demisto_ioc_to_xdr({}) assert output == {}, "demisto_ioc_to_xdr({})\n\treturns: " + str(d_sort(output)) + "\n\tinstead: {}" assert warnings.call_args.args[0] == "unexpected IOC format in key: 'value', {}" class TestXDRIOCToDemisto: data_test_xdr_expiration_to_demisto = [ (-1, "Never"), (1591142400000, "2020-06-03T00:00:00Z"), (1592142400000, "2020-06-14T13:46:40Z"), ] @pytest.mark.parametrize("xdr_expiration, demisto_expiration", data_test_xdr_expiration_to_demisto) def test_xdr_expiration_to_demisto(self, xdr_expiration, demisto_expiration): """ Given: - expiration in XDR format. Then: - expiration in demisto format. """ output = xdr_expiration_to_demisto(xdr_expiration) assert ( output == demisto_expiration ), f"xdr_expiration_to_demisto({xdr_expiration})\n\treturns: {output}\n\tinstead: {demisto_expiration}" # noqa: E501 data_test_xdr_ioc_to_demisto = [ ( { "RULE_ID": 863, "RULE_INSERT_TIME": 1591165763753, "RULE_MODIFY_TIME": 1591166095668, "RULE_SEVERITY": "SEV_010_INFO", "NUMBER_OF_HITS": 0, "RULE_SOURCE": "XSOAR TIM", "RULE_COMMENT": "", "RULE_STATUS": "DISABLED", "BS_STATUS": "DONE", "BS_TS": 1591165801230, "BS_RETRIES": 1, "RULE_EXPIRATION_TIME": -1, "IOC_TYPE": "HASH", "RULE_INDICATOR": "fa66f1e0e318b6d7b595b6cee580dc0d8e4ac38fbc8dbfcac6ad66dbe282832e", "REPUTATION": "GOOD", # noqa: E501 "RELIABILITY": None, "VENDORS": None, "KLASS": None, "IS_DEFAULT_TTL": False, "RULE_TTL": -1, "MARKED_DELETED": 0, }, { "value": "fa66f1e0e318b6d7b595b6cee580dc0d8e4ac38fbc8dbfcac6ad66dbe282832e", "type": "File", "score": 1, "fields": { "expirationdate": "Never", "tags": "Cortex XDR", "xdrstatus": "disabled", "sourceoriginalseverity": "INFO", }, }, ), ( { "RULE_ID": 861, "RULE_INSERT_TIME": 1591165763753, "RULE_MODIFY_TIME": 1591166095668, "RULE_SEVERITY": "SEV_010_INFO", "NUMBER_OF_HITS": 0, "RULE_SOURCE": "XSOAR TIM", "RULE_COMMENT": "", "RULE_STATUS": "DISABLED", "BS_STATUS": "DONE", "BS_TS": 1591165801784, "BS_RETRIES": 1, "RULE_EXPIRATION_TIME": -1, "IOC_TYPE": "DOMAIN_NAME", "RULE_INDICATOR": "test.com", "REPUTATION": "GOOD", # noqa: E501 "RELIABILITY": None, "VENDORS": None, "KLASS": None, "IS_DEFAULT_TTL": False, "RULE_TTL": -1, "MARKED_DELETED": 0, }, { "value": "test.com", "type": "Domain", "score": 1, "fields": { "expirationdate": "Never", "tags": "Cortex XDR", "xdrstatus": "disabled", "sourceoriginalseverity": "INFO", }, }, ), ( { "RULE_ID": 862, "RULE_INSERT_TIME": 1591165763753, "RULE_MODIFY_TIME": 1591166095668, "RULE_SEVERITY": "SEV_010_INFO", "NUMBER_OF_HITS": 0, "RULE_SOURCE": "XSOAR TIM", "RULE_COMMENT": "", "RULE_STATUS": "ENABLED", "BS_STATUS": "DONE", "BS_TS": 1591165801784, "BS_RETRIES": 1, "RULE_EXPIRATION_TIME": -1, "IOC_TYPE": "DOMAIN_NAME", "RULE_INDICATOR": "test.co.il", "REPUTATION": "SUSPICIOUS", "RELIABILITY": "A", "VENDORS": [{"vendor_name": "Cortex XDR - IOC", "reputation": "SUSPICIOUS", "reliability": "A"}], "KLASS": None, "IS_DEFAULT_TTL": False, "RULE_TTL": -1, "MARKED_DELETED": 0, }, { "value": "test.co.il", "type": "Domain", "score": 2, "fields": { "expirationdate": "Never", "tags": "Cortex XDR", "xdrstatus": "enabled", "sourceoriginalseverity": "INFO", }, }, ), ] @pytest.mark.parametrize("xdr_ioc, demisto_ioc", data_test_xdr_ioc_to_demisto) def test_xdr_ioc_to_demisto(self, xdr_ioc, demisto_ioc, mocker): """ Given: - IOC in XDR format. Then: - IOC in demisto format. """ mocker.patch.object(demisto, "searchIndicators", return_value={"total": 0}) output = xdr_ioc_to_demisto(xdr_ioc) del output["rawJSON"] assert ( output == demisto_ioc ), f"xdr_ioc_to_demisto({xdr_ioc})\n\treturns: {d_sort(output)}\n\tinstead: {d_sort(demisto_ioc)}" # noqa: E501 class TestCommands: # test commands full flow class TestIOCSCommand: def test_iocs_command_with_enable(self, mocker): """ Given: - enable command Then: - Verify enable command is called. """ mocker.patch.object(demisto, "command", return_value="xdr-iocs-enable") mocker.patch.object(demisto, "args", return_value={"indicator": "11.11.11.11"}) mocker.patch("XDR_iocs.Client.http_request", return_value={}) outputs = mocker.patch("XDR_iocs.return_outputs") enable_ioc = mocker.patch("XDR_iocs.prepare_enable_iocs", side_effect=prepare_enable_iocs) iocs_command(client) output = outputs.call_args.args[0] assert ( output == "IOCs command: enabled indicators='11.11.11.11'" ), f"enable command\n\tprints: {output}\n\tinstead: IOCs command: enabled indicators='11.11.11.11'." # noqa: E501 assert enable_ioc.call_count == 1, "enable command not called" def test_iocs_command_with_disable(self, mocker): """ Given: - disable command Then: - Verify disable command is called. """ mocker.patch.object(demisto, "command", return_value="xdr-iocs-disable") mocker.patch.object(demisto, "args", return_value={"indicator": "11.11.11.11"}) mocker.patch("XDR_iocs.Client.http_request", return_value={}) outputs = mocker.patch("XDR_iocs.return_outputs") disable_ioc = mocker.patch("XDR_iocs.prepare_disable_iocs", side_effect=prepare_disable_iocs) iocs_command(client) output = outputs.call_args.args[0] assert ( output == "IOCs command: disabled indicators='11.11.11.11'" ), f"enable command\n\tprints: {output}\n\tinstead: IOCs command: disabled indicators='11.11.11.11'." # noqa: E501 assert disable_ioc.call_count == 1, "disable command not called" def test_sync(self, mocker): http_request = mocker.patch.object(Client, "http_request") iocs, _ = TestCreateFile.get_all_iocs(TestCreateFile.data_test_create_file_sync, "txt") mocker.patch.object(demisto, "searchIndicators", returnvalue=iocs) mocker.patch("XDR_iocs.return_outputs") sync(client) assert http_request.call_args.args[0] == "sync_tim_iocs", "sync command url changed" @pytest.mark.parametrize( "zip_value, expected_file_name", [pytest.param(False, "xdr-sync-file", id="no zip"), pytest.param(True, "xdr-sync-file-zipped.zip", id="zip")], ) def test_get_sync_file(self, mocker, zip_value: bool, expected_file_name: str): iocs, _ = TestCreateFile.get_all_iocs(TestCreateFile.data_test_create_file_sync, "txt") mocker.patch.object(demisto, "searchIndicators", returnvalue=iocs) return_results_mock = mocker.patch("XDR_iocs.return_results") get_sync_file(zip=zip_value) assert return_results_mock.call_args[0][0]["File"] == expected_file_name def test_tim_insert_jsons(self, mocker): http_request = mocker.patch.object(Client, "http_request") http_request.return_value = {"reply": {"success": True}} mocker.patch.object(demisto, "getIntegrationContext", return_value={"time": "2020-06-03T00:00:00Z"}) iocs, _ = TestCreateFile.get_all_iocs(TestCreateFile.data_test_create_file_sync, "txt") mocker.patch.object(demisto, "searchIndicators", side_effect=[iocs, {"total": 0}]) mocker.patch("XDR_iocs.return_outputs") tim_insert_jsons(client) assert http_request.call_args.kwargs["url_suffix"] == "tim_insert_jsons/", "tim_insert_jsons command url changed" def test_get_changes(self, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={"ts": 1591142400000}) mocker.patch.object(demisto, "createIndicators") mocker.patch.object(demisto, "searchIndicators", return_value={"total": 0}) xdr_res = {"reply": [xdr_ioc[0] for xdr_ioc in TestXDRIOCToDemisto.data_test_xdr_ioc_to_demisto]} mocker.patch.object(Client, "http_request", return_value=xdr_res) get_changes(client) xdr_ioc_to_timeline([str(x[0].get("RULE_INDICATOR")) for x in TestXDRIOCToDemisto.data_test_xdr_ioc_to_demisto]) # noqa: E501 class TestParams: tags_test = [ ( {"value": "11.11.11.11", "indicator_type": "IP", "score": 2}, {"expiration_date": -1, "indicator": "11.11.11.11", "reputation": "SUSPICIOUS", "severity": "INFO", "type": "IP"}, {"tlp_color": ""}, "Cortex XDR", None, ), ( {"value": "11.11.11.11", "indicator_type": "IP", "score": 2}, {"expiration_date": -1, "indicator": "11.11.11.11", "reputation": "SUSPICIOUS", "severity": "INFO", "type": "IP"}, {"tag": "tag1"}, "tag1", None, ), ( {"value": "11.11.11.11", "indicator_type": "IP", "score": 2}, {"expiration_date": -1, "indicator": "11.11.11.11", "reputation": "SUSPICIOUS", "severity": "INFO", "type": "IP"}, {"feedTags": "tag2", "tlp_color": "AMBER"}, "tag2", "AMBER", ), ] @pytest.mark.parametrize("demisto_ioc, xdr_ioc, param_value, expected_tags, expected_tlp_color", tags_test) def test_feed_tags_and_tlp_color(self, demisto_ioc, xdr_ioc, param_value, expected_tags, expected_tlp_color, mocker): """ Given: - IOC in XDR format. Then: - IOC in demisto format. """ mocker.patch.object(demisto, "searchIndicators", return_value={"total": 0}) mocker.patch.object(demisto, "params", return_value=param_value) mocker.patch.object(demisto, "getIntegrationContext", return_value={"ts": 1591142400000}) mocker.patch.object(demisto, "searchIndicators", return_value={"total": 0}) outputs = mocker.patch.object(demisto, "createIndicators") Client.tag = demisto.params().get("feedTags", demisto.params().get("tag", Client.tag)) Client.tlp_color = demisto.params().get("tlp_color") client = Client({"url": "yana"}) xdr_res = {"reply": [xdr_ioc[0] for xdr_ioc in TestXDRIOCToDemisto.data_test_xdr_ioc_to_demisto]} mocker.patch.object(Client, "http_request", return_value=xdr_res) get_changes(client) output = outputs.call_args.args[0] assert output[0]["fields"]["tags"] == expected_tags assert output[0]["fields"].get("trafficlightprotocol") == expected_tlp_color def test_file_deleted_for_create_file_sync(mocker): file_path = "test" mocker.patch("XDR_iocs.get_temp_file", return_value=file_path) open(file_path, "w").close() def raise_function(*_args, **_kwargs): raise DemistoException(file_path) mocker.patch("XDR_iocs.create_file_sync", new=raise_function) with pytest.raises(DemistoException): get_sync_file(set_time=False, zip=False) assert os.path.exists(file_path) is False data_test_test_file_deleted = [(sync, "create_file_sync")] @pytest.mark.parametrize("method_to_test,iner_method", data_test_test_file_deleted) @freeze_time("2020-06-03T02:00:00Z") def test_file_deleted(mocker, method_to_test, iner_method): file_path = "test" mocker.patch("XDR_iocs.get_temp_file", return_value=file_path) open(file_path, "w").close() def raise_function(*_args, **_kwargs): raise DemistoException(file_path) mocker.patch(f"XDR_iocs.{iner_method}", new=raise_function) with pytest.raises(DemistoException): method_to_test(None) assert os.path.exists(file_path) is False data_test_batch = [ ([], 200, []), ([], 1, []), (range(3), 200, [[0, 1, 2]]), (range(3), 2, [[0, 1], [2]]), (range(4), 2, [[0, 1], [2, 3]]), (range(4), 1, [[0], [1], [2], [3]]), ] @pytest.mark.parametrize("input_enumerator, batch_size, expected_output", data_test_batch) def test_batch_iocs(input_enumerator, batch_size, expected_output): assert list(batch_iocs(input_enumerator, batch_size=batch_size)) == expected_output def test_overriding_severity_xsoar(): # back up class attributes xsoar_severity_field_backup = Client.xsoar_severity_field severity_value_backup = Client.severity # for testing Client.severity = "LOW" # constants custom_severity_field = "custom_severity_field" dummy_demisto_ioc = {"value": "1.1.1.1", "indicator_type": "FILE_123", "CustomFields": {custom_severity_field: "critical"}} # default behavior assert Client.override_severity is True assert demisto_ioc_to_xdr(dummy_demisto_ioc)["severity"] == "LOW" # behavior when override_severity is False Client.override_severity = False Client.xsoar_severity_field = custom_severity_field assert demisto_ioc_to_xdr(dummy_demisto_ioc)["severity"] == "CRITICAL" # behavior when there is no custom severity value dummy_demisto_ioc["CustomFields"][custom_severity_field] = None assert demisto_ioc_to_xdr(dummy_demisto_ioc)["severity"] == Client.severity # behavior when there is no custom severity field del dummy_demisto_ioc["CustomFields"][custom_severity_field] assert demisto_ioc_to_xdr(dummy_demisto_ioc)["severity"] == Client.severity # restore class attributes Client.override_severity = True Client.severity = severity_value_backup Client.xsoar_severity_field = xsoar_severity_field_backup def test_overriding_severity_xdr_to_demisto(): # back up class attributes xsoar_severity_field_backup = Client.xsoar_severity_field severity_value_backup = Client.severity Client.severity = "some hardcoded severity value" severity_field = "custom_severity_field" Client.xsoar_severity_field = severity_field dummy_xdr_ioc = {"IOC_TYPE": "IP", "RULE_EXPIRATION_TIME": -1, "RULE_SEVERITY": "SEV_050_CRITICAL"} # default behavior assert Client.override_severity is True # this should always be the default assert xdr_ioc_to_demisto(dummy_xdr_ioc)["fields"][severity_field] == Client.severity # behavior when override_severity is False Client.override_severity = False assert xdr_ioc_to_demisto(dummy_xdr_ioc)["fields"][severity_field] == "CRITICAL" # restore class attributes Client.override_severity = True Client.severity = severity_value_backup Client.xsoar_severity_field = xsoar_severity_field_backup @pytest.mark.parametrize("value", ("info", "Info", "informational", "INformationAL")) def test_severity_fix_info(value: str): """ given a severity value that should be fixed to INFO when calling validate_fix_severity_value then make sure the value returned INFO """ assert validate_fix_severity_value(value) == "INFO" @pytest.mark.parametrize("value", ("", "a", "foo", "severity", "infoo", "informationall")) def test_severity_validate(value: str): with pytest.raises(DemistoException): validate_fix_severity_value(value) def test_parse_demisto_comments__default(): """ Given a custom field name, and comma-separated comments in it When parsing a comment of the default comment field Then check the output values """ from XDR_iocs import _parse_demisto_comments comment_value = "here be comment" assert _parse_demisto_comments( ioc={Client.xsoar_comments_field: [{"type": "IndicatorCommentRegular", "content": comment_value}]}, comment_field_name=Client.xsoar_comments_field, comments_as_tags=False, ) == [comment_value] def test_parse_demisto_comments__default_empty(): """ Given a custom field name, and comma-separated comments in it When parsing a comment of the default comment field Then check parsing a comment results in None. """ from XDR_iocs import _parse_demisto_comments assert _parse_demisto_comments(ioc={}, comment_field_name=Client.xsoar_comments_field, comments_as_tags=False) == [""] def test_parse_demisto_comments__default_as_tag(): """ Given a custom field name When parsing a comment of the default comment field, passing comments_as_tags=True Then make sure an appropriate exception is raised """ from XDR_iocs import _parse_demisto_comments with pytest.raises(DemistoException) as exc: _parse_demisto_comments( ioc={Client.xsoar_comments_field: [{"type": "IndicatorCommentRegular", "content": "whatever"}]}, comment_field_name=Client.xsoar_comments_field, comments_as_tags=True, ) assert ( exc.value.message == "When specifying comments_as_tags=True, the xsoar_comment_field cannot be `comments`)." "Set a different value." ) @pytest.mark.parametrize( "comment_value,comments_as_tags,expected", ( ("hello", True, ["hello"]), ("hello", False, ["hello"]), ("hello,world", True, ["hello", "world"]), ("hello,world", False, ["hello,world"]), ), ) def test_parse_demisto_comments__custom_field(comment_value: str, comments_as_tags: bool, expected: str): """ Given a custom field name When parsing a comment of a non-default comment field Then make sure the comment is parsed as expected """ from XDR_iocs import _parse_demisto_comments comment_field = "comment_field" Client.xsoar_comments_field = "comment_field" assert ( _parse_demisto_comments( ioc={"CustomFields": {comment_field: comment_value}}, comment_field_name=comment_field, comments_as_tags=comments_as_tags, ) == expected ) @pytest.mark.parametrize("comments_as_tags", (True, False)) def test_parse_demisto_comments__custom_field_empty_value(comments_as_tags: bool): """ Given a custom field name, and an empty value as When parsing a comment of a non-default comment field Then make sure the comment is parsed as expected """ from XDR_iocs import _parse_demisto_comments comment_field = "comment_field" assert _parse_demisto_comments( ioc={"CustomFields": {comment_field: ""}}, comment_field_name=comment_field, comments_as_tags=comments_as_tags ) == [""] @pytest.mark.parametrize("comments_as_tags", (True, False)) def test_parse_demisto_comments__custom_field_missing(comments_as_tags: bool): """ Given a custom field name, which does not exist in the IOC When parsing a comment Then make sure the comment is parsed as expected """ from XDR_iocs import _parse_demisto_comments assert _parse_demisto_comments( ioc={"CustomFields": {}}, comment_field_name="comment_field", comments_as_tags=comments_as_tags ) == [""] @pytest.mark.parametrize( "raw_comment,comments_as_tags,expected_comment", ( ("hello", True, ["hello"]), ("hello", False, ["hello"]), ("hello,world", True, ["hello", "world"]), ("hello,world", False, ["hello,world"]), ("", True, []), ("", False, []), ), ) def test_parse_xdr_comments(raw_comment: str | list[str], comments_as_tags: bool, expected_comment: str | None): """ Given a custom field name, and comma-separated comments in it When converting an XSOAR IOC to XDR Then check the output values """ from XDR_iocs import _parse_xdr_comments assert _parse_xdr_comments(raw_comment, comments_as_tags) == expected_comment @pytest.mark.parametrize( "validation_errors, expected_str", ( ( [ { "indicator": "1.1.1.1", "error": "Expiration time 1696323079000 is invalid; expiration date cannot be in the past", }, { "indicator": "3.3.3.3", "error": "Expiration time 1696150302000 is invalid; expiration date cannot be in the past", }, ], "Expiration time 1696323079000 is invalid; expiration date cannot be in the past", ), ( [ { "indicator": "1.1.1.1", "error": "Expiration time 1696323079000 is invalid; expiration date cannot be in the past", } ], "Expiration time 1696323079000 is invalid; expiration date cannot be in the past", ), ([], ""), ), ) def test_create_validation_errors_response(validation_errors, expected_str): """ Given validation errors that returned from the server. When pushing XSOAR IOC to XDR Then check the parsed error """ from XDR_iocs import create_validation_errors_response assert expected_str in create_validation_errors_response(validation_errors) def test_parse_demisto_comments_url_xsoar_6_default(mocker): """ Given: - xsoar version 6, a custom field name, and comma-separated comments in it When: - parsing a comment of the url indicator field Then: - check the output values """ from XDR_iocs import _parse_demisto_comments inc_id = "111111" mocker.patch.object(demisto, "demistoUrls", return_value={"server": "url"}) Client.add_link_as_a_comment = True assert _parse_demisto_comments(ioc={"id": inc_id}, comment_field_name="", comments_as_tags=False) == [ f"url/#/indicator/{inc_id}" ] def test_parse_demisto_comments_url_xsoar_8_default(mocker): """ Given: - xsoar version that is greater than 8, a custom field name, and comma-separated comments in it When: - parsing a comment of the url indicator field Then: - check the output values """ import XDR_iocs os.environ["CRTX_HTTP_PROXY"] = "xsoar_8_proxy" inc_id = "111111" mocker.patch.object(demisto, "demistoUrls", return_value={"server": "url"}) mocker.patch.object(XDR_iocs, "is_xsoar_saas", return_value=True) Client.add_link_as_a_comment = True assert XDR_iocs._parse_demisto_comments(ioc={"id": inc_id}, comment_field_name="", comments_as_tags=False) == [ f"url/indicator/{inc_id}" ] def test_parse_demisto_list_of_comments_default(mocker): """ Given a custom field name, and comma-separated comments in it When parsing a comment of the url indicator field Then check the output values """ from XDR_iocs import _parse_demisto_comments inc_id = "111111" comment_value = "here be comment" mocker.patch.object(demisto, "demistoUrls", return_value={"server": "url"}) Client.xsoar_comments_field, Client.add_link_as_a_comment = "comments", True assert _parse_demisto_comments( ioc={Client.xsoar_comments_field: [{"type": "IndicatorCommentRegular", "content": comment_value}], "id": inc_id}, comment_field_name=Client.xsoar_comments_field, comments_as_tags=False, ) == [f"{comment_value}, url/#/indicator/{inc_id}"] @patch("XDR_iocs.demisto.params", return_value={"feed": True, "feedFetchInterval": "14"}) def test_module_fail_with_fetch_interval(mocker): """ Given The demisto.params() returns parameters with feed set to True and feedFetchInterval set to '15'. When The module_test() function is called. Then Raise a DemistoException with the message: "'Feed Fetch Interval' parameter should be 15 or larger." """ from XDR_iocs import module_test with pytest.raises(DemistoException) as e: module_test(client) assert e.value.message == ( "`Feed Fetch Interval` is set to 14. Setting `Feed Fetch Interval` to less " "then 15 minutes could lead to internal error from xdr side." ) ioc_example = { "value": "malicious.com", "modified": "2023-09-09T12:00:00Z", "indicator_type": "Domain", "score": 3, "expiration": "2024-09-09T12:00:00Z", "aggregatedReliability": ["A"], "moduleToFeedMap": {"FeedA": {"reliability": "A"}}, "CustomFields": { "threattypes": [{"threatcategory": "Malware"}], "xdrstatus": "enabled", "sourceoriginalseverity": "high", }, } @patch("XDR_iocs.get_integration_context", return_value={"time": "2024-09-10T12:13:57Z"}) @patch("XDR_iocs.get_iocs_generator", return_value=[ioc_example]) @patch("XDR_iocs.Client.http_request", return_value={"reply": {"success": True}}) @patch("XDR_iocs.set_integration_context", return_value={}) @patch("XDR_iocs.update_integration_context_override", return_value={}) def test_xdr_iocs_sync_command_sync_for_fetch( mock_update_integration_context_override, mock_set_integration_context, mock_http_request, mock_get_iocs_generator, mock_get_integration_context, ): """ Given xdr_iocs_sync_command function is called with is_first_stage_sync=true, called_from_fetch=true When the http_request is successful, Then the update_integration_context function should be called with update_is_first_sync_phase='false' """ xdr_iocs_sync_command(client, is_first_stage_sync=True, called_from_fetch=True) mock_update_integration_context_override.assert_called_with(update_is_first_sync_phase="false") @patch("XDR_iocs.get_integration_context", return_value={"time": "2024-09-10T12:13:57Z"}) @patch("XDR_iocs.get_iocs_generator", return_value=[ioc_example]) @patch("XDR_iocs.Client.http_request", return_value={"reply": {"success": False}}) @patch("XDR_iocs.set_integration_context", return_value={}) @patch("XDR_iocs.update_integration_context", return_value={}) def test_xdr_iocs_sync_command_sync_for_fetch_fails( mock_update_integration_context, mock_set_integration_context, mock_http_request, mock_get_iocs_generator, mock_get_integration_context, ): """ Given that the xdr_iocs_sync_command function is called with is_first_stage_sync=true, called_from_fetch=true When the http_request fails Then Raises DemistoException """ with pytest.raises(DemistoException) as e: xdr_iocs_sync_command(client, is_first_stage_sync=True, called_from_fetch=True) assert e.value.message == ( "Failed to sync indicators with error Response status was not success, response={'reply': {'success': False}}." ) @patch("XDR_iocs.get_integration_context", return_value={"time": "2024-09-10T12:13:57Z"}) @patch("XDR_iocs.get_iocs_generator", return_value=[ioc_example]) @patch( "XDR_iocs.Client.http_request", return_value={ "reply": { "success": True, "validation_errors": [{"indicator": "123", "error": "error1"}, {"indicator": "456", "error": "error2"}], } }, ) @patch("XDR_iocs.set_integration_context", return_value={}) @patch("XDR_iocs.update_integration_context_override", return_value={}) @patch("XDR_iocs.demisto.debug") def test_xdr_iocs_sync_command_sync_for_fetch_with_validation_errors( mock_demisto_debug, mock_update_integration_context_override, mock_set_integration_context, mock_http_request, mock_get_iocs_generator, mock_get_integration_context, ): """ Given the xdr_iocs_sync_command function is called with is_first_stage_sync=true, called_from_fetch=true When There are validation errors in the response Then update_integration_context should be called with update_is_first_sync_phase='false', and a debug message should be logged indicating the validation errors. """ xdr_iocs_sync_command(client, is_first_stage_sync=True, called_from_fetch=True) mock_update_integration_context_override.assert_called_with(update_is_first_sync_phase="false") debug_calls = [call.args[0] for call in mock_demisto_debug.call_args_list] expected_debug_message = ( "pushing IOCs to XDR:The following 2 IOCs were not pushed due to following errors:123: error1.456: error2." ) assert any(expected_debug_message in call for call in debug_calls), f"Expected debug message not found in: {debug_calls}" @patch( "XDR_iocs.get_integration_context", return_value={ "time": "2024-09-10T12:13:57Z", "ts": "1234567", "is_first_sync_phase": True, "search_after": ["1234", "098765"], }, ) @patch("XDR_iocs.set_integration_context") def test_update_integration_context(mock_set_integration_context, mock_get_integration_context): """ Given integration_context has some values When update_integration_context (of XDR_iocs) is being called with all args Then The integration context is being changed """ fixed_datetime = datetime(2024, 9, 10, 12, 0, 0, tzinfo=timezone.utc) update_integration_context_override( update_sync_time_with_datetime=fixed_datetime, update_is_first_sync_phase="false", update_search_after_array=["765", "000"], ) mock_set_integration_context.assert_called_with( {"time": "2024-09-10T12:00:00Z", "ts": 1725969600000, "is_first_sync_phase": False, "search_after": ["765", "000"]} ) @patch("XDR_iocs.get_integration_context") @patch("XDR_iocs.sync") def test_xdr_iocs_sync_command(mock_sync, mock_integration_context): """ Given: - first_time is true - as this is the first sync phase - integration context is empty When: - xdr_iocs_sync_command is called not from a fetch_indicators command Then: - The sync command is being called """ client = MagicMock() # Test case 1: first_time is true xdr_iocs_sync_command(client, first_time=True) mock_sync.assert_called_with(client, batch_size=4000) mock_sync.reset_mock() # Test case 2: integration context is empty mock_integration_context.return_value = {} xdr_iocs_sync_command(client) mock_sync.assert_called_with(client, batch_size=4000) @patch("XDR_iocs.get_integration_context") @patch("XDR_iocs.sync_for_fetch") def test_xdr_iocs_sync_command_from_fetch(mock_sync_for_fetch, mock_integration_context): """ Given: - first_time is true- as this is the first sync phase - integration context is empty When: - xdr_iocs_sync_command is called from a fetch_indicators command Then: - The sync_for_fetch command is being called """ client = MagicMock() xdr_iocs_sync_command(client, called_from_fetch=True, is_first_stage_sync=True) mock_sync_for_fetch.assert_called_with(client, batch_size=4000) mock_sync_for_fetch.reset_mock() mock_integration_context.return_value = {} xdr_iocs_sync_command(client, called_from_fetch=True) mock_sync_for_fetch.assert_called_with(client, batch_size=4000) @pytest.mark.parametrize( "indicators_input, search_results, expected_iocs, expected_warning, expected_info", [ ( "indicator1,indicator2", [{"iocs": ["ioc1"]}, {"iocs": ["ioc2"]}], ["ioc1", "ioc2"], None, "get_indicators found 2 IOCs", ), ( "indicator1,indicator2", [{"iocs": ["ioc1"]}, {"iocs": []}], ["ioc1"], "1 indicators were not found: indicator2", "get_indicators found 1 IOCs", ), ( "indicator1,indicator2", [{"iocs": None}, {"iocs": None}], [], "2 indicators were not found: indicator1,indicator2", "2 indicators were not found: indicator1,indicator2", ), ("", [], [], None, None), ], ) @patch("XDR_iocs.demisto.debug") @patch("XDR_iocs.demisto.info") @patch("XDR_iocs.return_warning") @patch("XDR_iocs.IndicatorsSearcher") def test_get_indicators( mock_IndicatorsSearcher, mock_return_warning, mock_info, mock_debug, indicators_input, search_results, expected_iocs, expected_warning, expected_info, ): mock_searcher_instance = MagicMock() mock_searcher_instance.search_indicators_by_version.side_effect = search_results mock_IndicatorsSearcher.return_value = mock_searcher_instance result = get_indicators(indicators_input) assert result == expected_iocs if expected_warning: mock_return_warning.assert_called_with(expected_warning) else: mock_return_warning.assert_not_called() if expected_info: mock_info.assert_called_with(expected_info) else: mock_info.assert_not_called() @pytest.mark.parametrize("is_xsoar_saas, expected_link", ((True, ["url/indicator/111"]), (False, ["url/#/indicator/111"]))) def test_create_an_indicator_link(mocker, is_xsoar_saas: bool, expected_link: str): """ Given: - indicator id and a bool argument is_xsoar_saas which presents if xsaor is a saas version or not When: - creating an indicator link Then: - verify the link according to the XSAOR version """ import XDR_iocs mocker.patch.object(demisto, "demistoUrls", return_value={"server": "url"}) mocker.patch.object(XDR_iocs, "is_xsoar_saas", return_value=is_xsoar_saas) assert XDR_iocs.create_an_indicator_link(ioc={"id": "111"}) == expected_link @pytest.mark.parametrize( "xsoar_comment_field, expected_result", ( (["indicator_link"], ("comments", True)), (["comments"], ("comments", False)), (["comments", "indicator_link"], ("comments", True)), ), ) def test_parse_xsoar_field_name_and_link(xsoar_comment_field: list[str], expected_result: tuple[str, bool]): """ Given: - xsoar_comment_field When: - parsing xsoar_comment_field by our logic Then: - verify the function parses the xsoar_comment_field as expected """ import XDR_iocs result = XDR_iocs.parse_xsoar_field_name_and_link(xsoar_comment_field) assert result == expected_result @pytest.mark.parametrize( "xsoar_comment_field, informative_message", ( ( ["comments", "not_indicator_link"], "The parameter xsoar_comment_field=['comments', 'not_indicator_link'] " "should only contain the field name," " or the field name with the phrase indicator_link, separated by a comma.", ), (["a", "b", "c"], ("The parameter xsoar_comment_field=['a', 'b', 'c'] cannot contain more than two values")), ), ) def test_parse_xsoar_field_name_and_link_exceptions(xsoar_comment_field: list[str], informative_message: str): """ Given: - invalid xsoar_comment_field and the expected_result When: - parsing xsoar_comment_field by our logic Then: - verify the function throws a DemistoException with informative message """ import XDR_iocs with pytest.raises(DemistoException) as de: XDR_iocs.parse_xsoar_field_name_and_link(xsoar_comment_field) assert de.message == informative_message