import copy
import json
import os
import zipfile
from typing import Any
from unittest.mock import Mock, patch
import demistomock as demisto
import pytest
from CommonServerPython import Common, DemistoException, pascalToSpace, tableToMarkdown
from CoreIRApiModule import (
XSOAR_RESOLVED_STATUS_TO_XDR,
CoreClient,
add_tag_to_endpoints_command,
change_user_role_command,
enrich_error_message_id_group_role,
get_incidents_command,
handle_outgoing_issue_closure,
isolate_endpoint_command,
list_risky_users_or_host_command,
list_roles_command,
list_user_groups_command,
list_users_command,
parse_user_groups,
quarantine_files_command,
remove_tag_from_endpoints_command,
)
from freezegun import freeze_time
from pytest_mock import MockerFixture
test_client = CoreClient(base_url="https://test_api.com/public_api/v1", headers={})
Core_URL = "https://api.xdrurl.com"
POWERSHELL_COMMAND_CASES = [
pytest.param(
"Write-Output 'Hello, world, it`s me!'",
"powershell -Command \"Write-Output ''Hello, world, it`s me!''\"",
id="Hello World message",
),
pytest.param(
r"New-Item -Path 'C:\Users\User\example.txt' -ItemType 'File'",
"powershell -Command \"New-Item -Path ''C:\\Users\\User\\example.txt'' -ItemType ''File''\"",
id="New file in path with backslashes",
),
pytest.param(
"$message = 'This is a test with special chars: `&^%$#@!'; Write-Output $message",
"powershell -Command \"$message = ''This is a test with special chars: `&^%$#@!''; Write-Output $message\"",
id="Special characters message",
),
pytest.param(
(
"$users = @(JohnDoe) -split ';'; query user | Select-Object -Skip 1 | "
"ForEach-Object { $sessionInfo = $_ -split '\s+' | "
"Where-Object { $_ -ne '' -and $_ -notlike 'Disc' }; "
"if ($sessionInfo.Length -ge 6) { $username = $sessionInfo[0].TrimStart('>'); "
"$sessionId = $sessionInfo[2]; if ($users -contains $username) { logoff $sessionId } } }"
),
(
"powershell -Command \"$users = @(JohnDoe) -split '';''; query user | Select-Object -Skip 1 | "
"ForEach-Object { $sessionInfo = $_ -split ''\\s+'' | "
"Where-Object { $_ -ne '''' -and $_ -notlike ''Disc'' }; "
"if ($sessionInfo.Length -ge 6) { $username = $sessionInfo[0].TrimStart(''>''); "
'$sessionId = $sessionInfo[2]; if ($users -contains $username) { logoff $sessionId } } }"'
),
id="End RDP session for users",
),
]
""" HELPER FUNCTIONS """
def load_test_data(json_path):
with open(json_path) as f:
return json.load(f)
def get_incident_extra_data_by_status(incident_id, alerts_limit):
"""
The function simulate the client.get_incident_extra_data method for the test_fetch_incidents_filtered_by_status.
The function got the incident_id, and return the json file by the incident id.
"""
if incident_id == "1":
incident_extra_data = load_test_data("./test_data/get_incident_extra_data.json")
else:
incident_extra_data = load_test_data("./test_data/get_incident_extra_data_new_status.json")
return incident_extra_data["reply"]
""" TESTS FUNCTIONS """
# Note this test will fail When: run locally (in pycharm/vscode)
# as it assumes the machine (docker image) has UTC timezone set
@pytest.mark.parametrize(
argnames="time_to_convert, expected_value",
argvalues=[
("1322683200000", 1322683200000),
("2018-11-06T08:56:41", 1541494601000),
],
)
def test_convert_time_to_epoch(time_to_convert, expected_value):
from CoreIRApiModule import convert_time_to_epoch
assert convert_time_to_epoch(time_to_convert) == expected_value
def return_extra_data_result(*args):
if args[1].get("incident_id") == "2":
raise Exception("Rate limit exceeded")
else:
incident_from_extra_data_command = load_test_data("./test_data/incident_example_from_extra_data_command.json")
return {}, {}, {"incident": incident_from_extra_data_command}
def test_retrieve_all_endpoints(mocker):
"""
Given:
- endpoints is populated with the first round.
When:
- Retrieve_all_endpoints is called.
Then:
- Retrieve all endpoints.
"""
from CoreIRApiModule import retrieve_all_endpoints
mock_endpoints_page_1 = {"reply": {"endpoints": [{"id": 1, "hostname": "endpoint1"}]}}
mock_endpoints_page_2 = {"reply": {"endpoints": [{"id": 2, "hostname": "endpoint2"}]}}
mock_endpoints_page_3 = {"reply": {"endpoints": []}}
http_request = mocker.patch.object(test_client, "_http_request")
http_request.side_effect = [
mock_endpoints_page_1,
mock_endpoints_page_2,
mock_endpoints_page_3,
]
endpoints = retrieve_all_endpoints(
client=test_client,
endpoints=[{"id": 2, "hostname": "endpoint2"}],
endpoint_id_list=[],
dist_name=None,
ip_list=[],
public_ip_list=[],
group_name=None,
platform=None,
alias_name=None,
isolate=None,
hostname=None,
page_number=0,
limit=10,
first_seen_gte=None,
first_seen_lte=None,
last_seen_gte=None,
last_seen_lte=None,
sort_by_first_seen=None,
sort_by_last_seen=None,
status=None,
username=None,
)
assert len(endpoints) == 3
assert endpoints[1]["hostname"] == "endpoint1"
def test_get_endpoints_command(mocker):
"""
When:
- Retrieve_all_endpoints is called.
Then:
- Retrieve all endpoints.
"""
from CoreIRApiModule import get_endpoints_command
mock_endpoints_page_1 = {"reply": {"endpoints": [{"endpoint_id": "1", "hostname": "endpoint1"}]}}
mock_endpoints_page_2 = {"reply": {"endpoints": [{"endpoint_id": "2", "hostname": "endpoint2"}]}}
mock_endpoints_page_3 = {"reply": {"endpoints": []}}
http_request = mocker.patch.object(test_client, "_http_request")
http_request.side_effect = [
mock_endpoints_page_1,
mock_endpoints_page_2,
mock_endpoints_page_3,
]
args = {"all_results": "true"}
result = get_endpoints_command(test_client, args)
assert len(result.raw_response) == 2
assert result.raw_response[0]["endpoint_id"] == "1"
assert result.raw_response[1]["endpoint_id"] == "2"
def test_get_endpoints_command_with_duplicates(mocker):
"""
Given:
- API returns duplicate endpoints across pages (simulating pagination issue).
When:
- get_endpoints_command is called with all_results=true.
Then:
- Duplicates should be removed and only unique endpoints returned.
- Debug log should indicate duplicates were found and removed.
"""
from CoreIRApiModule import get_endpoints_command
# Simulate API returning duplicates: endpoint1 appears in both page 1 and page 2
mock_endpoints_page_1 = {"reply": {"endpoints": [{"endpoint_id": "1111", "hostname": "endpoint1"}]}}
mock_endpoints_page_2 = {
"reply": {
"endpoints": [{"endpoint_id": "1111", "hostname": "endpoint1"}, {"endpoint_id": "2222", "hostname": "endpoint2"}]
}
}
mock_endpoints_page_3 = {"reply": {"endpoints": []}}
http_request = mocker.patch.object(test_client, "_http_request")
http_request.side_effect = [mock_endpoints_page_1, mock_endpoints_page_2, mock_endpoints_page_3]
info_mock = mocker.patch.object(demisto, "info")
args = {"all_results": "true"}
result = get_endpoints_command(test_client, args)
# Verify only unique endpoints are returned
assert len(result.raw_response) == 2
assert result.raw_response == [
{"endpoint_id": "1111", "hostname": "endpoint1"},
{"endpoint_id": "2222", "hostname": "endpoint2"},
]
# Verify deduplication was logged
info_calls = [str(call) for call in info_mock.call_args_list]
assert any("removed 1 duplicate endpoint(s)" in str(call) for call in info_calls)
def test_convert_to_hr_timestamps():
"""
Given
- Endpoints results.
When:
- convert_to_hr_timestamps is called.
Then:
- Convert to an hr date.
"""
from CoreIRApiModule import convert_timestamps_to_datestring
expected_first_seen = "2019-12-08T09:06:09.000Z"
expected_last_seen = "2019-12-09T07:10:04.000Z"
endpoints_res = load_test_data("./test_data/get_endpoints.json").get("reply").get("endpoints")
converted_endpoint = convert_timestamps_to_datestring(endpoints_res)[0]
assert converted_endpoint.get("first_seen") == expected_first_seen
assert converted_endpoint.get("last_seen") == expected_last_seen
def test_get_endpoints(requests_mock):
from CoreIRApiModule import CoreClient, get_endpoints_command
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoint/", json=get_endpoints_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"hostname": "foo", "page": 1, "limit": 3}
res = get_endpoints_command(client, args)
assert (
get_endpoints_response.get("reply").get("endpoints")
== res.outputs["CoreApiModule.Endpoint(val.endpoint_id == obj.endpoint_id)"]
)
def test_get_all_endpoints_using_limit(requests_mock):
"""
Given:
The default arguments for the get endpoints command: limit = 1, page = 0, sort_order = 'asc'.
When:
Calling the get_endpoints_command function
Then:
a. Make sure the 'get_endpoints' API is not called (not to be confused with get_endpoint - see last comment
here: https://jira-hq.paloaltonetworks.local/browse/XSUP-15995)
b. Make sure the returned result as in the expected format.
"""
from CoreIRApiModule import CoreClient, get_endpoints_command
get_endpoints_response = load_test_data("./test_data/get_all_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoint/", json=get_endpoints_response)
get_endpoints_mock = requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoints/")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"limit": 1, "page": 0, "sort_order": "asc"}
res = get_endpoints_command(client, args)
expected_endpoint = get_endpoints_response.get("reply").get("endpoints")
assert not get_endpoints_mock.called
assert res.outputs["CoreApiModule.Endpoint(val.endpoint_id == obj.endpoint_id)"] == expected_endpoint
def test_endpoint_command(requests_mock):
from CoreIRApiModule import CoreClient, endpoint_command
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoint/", json=get_endpoints_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"id": "identifier"}
outputs = endpoint_command(client, args)
get_endpoints_response = {
Common.Endpoint.CONTEXT_PATH: [
{
"ID": "1111",
"Hostname": "ip-3.3.3.3",
"IPAddress": ["3.3.3.3"],
"OS": "Linux",
"Vendor": "CoreApiModule",
"Status": "Online",
"IsIsolated": "No",
}
]
}
results = outputs[0].to_context()
for key, value in results.get("EntryContext", {}).items():
assert get_endpoints_response[key] == value
assert results.get("EntryContext") == get_endpoints_response
def test_isolate_endpoint(requests_mock):
from CoreIRApiModule import CoreClient, isolate_endpoint_command
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/get_endpoint/",
json={"reply": {"endpoints": [{"endpoint_id": "1111", "endpoint_status": "CONNECTED"}]}},
)
isolate_endpoint_response = load_test_data("./test_data/isolate_endpoint.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/isolate", json=isolate_endpoint_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_id": "1111"}
res = isolate_endpoint_command(client, args)
assert res.readable_output == "The isolation request has been submitted successfully on Endpoint 1111.\n"
def test_isolate_endpoint_unconnected_machine(requests_mock, mocker):
from CoreIRApiModule import CoreClient, isolate_endpoint_command
# return_error_mock = mocker.patch(RETURN_ERROR_TARGET)
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/get_endpoint/",
json={"reply": {"endpoints": [{"endpoint_id": "1111", "endpoint_status": "DISCONNECTED"}]}},
)
isolate_endpoint_response = load_test_data("./test_data/isolate_endpoint.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/isolate", json=isolate_endpoint_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_id": "1111", "suppress_disconnected_endpoint_error": False}
with pytest.raises(
ValueError,
match="Error: Endpoint 1111 is disconnected and therefore can not be isolated.",
):
isolate_endpoint_command(client, args)
def test_unisolate_endpoint(requests_mock):
from CoreIRApiModule import CoreClient, unisolate_endpoint_command
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/get_endpoint/",
json={"reply": {"endpoints": [{"endpoint_id": "1111", "endpoint_status": "CONNECTED"}]}},
)
unisolate_endpoint_response = load_test_data("./test_data/unisolate_endpoint.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/unisolate",
json=unisolate_endpoint_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_id": "1111"}
res = unisolate_endpoint_command(client, args)
assert res.readable_output == "The un-isolation request has been submitted successfully on Endpoint 1111.\n"
def test_unisolate_endpoint_unconnected_machine(requests_mock):
from CoreIRApiModule import CoreClient, unisolate_endpoint_command
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/get_endpoint/",
json={"reply": {"endpoints": [{"endpoint_id": "1111", "endpoint_status": "DISCONNECTED"}]}},
)
unisolate_endpoint_response = load_test_data("./test_data/unisolate_endpoint.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/unisolate",
json=unisolate_endpoint_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_id": "1111", "suppress_disconnected_endpoint_error": True}
res = unisolate_endpoint_command(client, args)
assert res.readable_output == "Warning: un-isolation action is pending for the following disconnected endpoint: 1111."
def test_unisolate_endpoint_pending_isolation(requests_mock):
from CoreIRApiModule import CoreClient, unisolate_endpoint_command
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/get_endpoint/",
json={"reply": {"endpoints": [{"endpoint_id": "1111", "is_isolated": "AGENT_PENDING_ISOLATION"}]}},
)
unisolate_endpoint_response = load_test_data("./test_data/unisolate_endpoint.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/unisolate",
json=unisolate_endpoint_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_id": "1111"}
with pytest.raises(
ValueError,
match="Error: Endpoint 1111 is pending isolation and therefore can not be un-isolated.",
):
unisolate_endpoint_command(client, args)
def test_get_distribution_url(requests_mock):
from CoreIRApiModule import CoreClient, get_distribution_url_command
get_distribution_url_response = load_test_data("./test_data/get_distribution_url.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/distributions/get_dist_url/",
json=get_distribution_url_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"distribution_id": "1111", "package_type": "x86"}
result = get_distribution_url_command(client, args)
expected_url = get_distribution_url_response.get("reply").get("distribution_url")
assert result.outputs == {"id": "1111", "url": expected_url}
assert result.readable_output == f"[Distribution URL]({expected_url})"
def test_download_distribution(requests_mock):
"""
Given:
- Core client
- Distribution ID and package type
When:
- Running xdr-download-distribution command
Then:
- Verify filename
- Verify readable output is as expected
"""
from CoreIRApiModule import CoreClient, get_distribution_url_command
get_distribution_url_response = load_test_data("./test_data/get_distribution_url.json")
dummy_url = "https://xdrdummyurl.com/11111-distributions/11111/sh"
requests_mock.post(
f"{Core_URL}/public_api/v1/distributions/get_dist_url/",
json=get_distribution_url_response,
)
requests_mock.get(dummy_url, content=b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1")
installer_file_name = "xdr-agent-install-package.msi"
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"distribution_id": "1111",
"package_type": "x86",
"download_package": "true",
}
result = get_distribution_url_command(client, args)
assert result[0]["File"] == installer_file_name
assert result[1].readable_output == "Installation package downloaded successfully."
def test_get_audit_management_logs(requests_mock):
from CoreIRApiModule import CoreClient, get_audit_management_logs_command
get_audit_management_logs_response = load_test_data("./test_data/get_audit_management_logs.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/audits/management_logs/",
json=get_audit_management_logs_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"email": "woo@demisto.com", "limit": "3", "timestamp_gte": "3 month"}
readable_output, outputs, _ = get_audit_management_logs_command(client, args)
expected_outputs = get_audit_management_logs_response.get("reply").get("data")
assert outputs["CoreApiModule.AuditManagementLogs(val.AUDIT_ID == obj.AUDIT_ID)"] == expected_outputs
def test_get_audit_agent_reports(requests_mock):
from CoreIRApiModule import CoreClient, get_audit_agent_reports_command
get_audit_agent_reports_response = load_test_data("./test_data/get_audit_agent_report.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/audits/agents_reports/",
json=get_audit_agent_reports_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_names": "woo.demisto", "limit": "3", "timestamp_gte": "3 month"}
readable_output, outputs, _ = get_audit_agent_reports_command(client, args)
expected_outputs = get_audit_agent_reports_response.get("reply").get("data")
assert outputs["CoreApiModule.AuditAgentReports"] == expected_outputs
assert outputs["Endpoint(val.ID && val.ID == obj.ID && val.Vendor == obj.Vendor)"] == [
{"ID": "1111", "Hostname": "1111.eu-central-1"},
{"ID": "1111", "Hostname": "1111.eu-central-1"},
{"ID": "1111", "Hostname": "1111.eu-central-1"},
]
def test_get_distribution_status(requests_mock):
from CoreIRApiModule import CoreClient, get_distribution_status_command
get_distribution_status_response = load_test_data("./test_data/get_distribution_status.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/distributions/get_status/",
json=get_distribution_status_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"distribution_ids": "588a56de313549b49d70d14d4c1fd0e3"}
readable_output, outputs, _ = get_distribution_status_command(client, args)
assert outputs == {
"CoreApiModule.Distribution(val.id == obj.id)": [{"id": "588a56de313549b49d70d14d4c1fd0e3", "status": "Completed"}]
}
def test_get_distribution_versions(requests_mock):
from CoreIRApiModule import CoreClient, get_distribution_versions_command
get_distribution_versions_response = load_test_data("./test_data/get_distribution_versions.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/distributions/get_versions/",
json=get_distribution_versions_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
readable_output, outputs, _ = get_distribution_versions_command(client, args={})
assert outputs == {
"CoreApiModule.DistributionVersions": {
"windows": ["7.0.0.27797"],
"linux": ["7.0.0.1915"],
"macos": ["7.0.0.1914"],
}
}
def test_create_distribution(requests_mock):
from CoreIRApiModule import CoreClient, create_distribution_command
create_distribution_response = load_test_data("./test_data/create_distribution.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/distributions/create/",
json=create_distribution_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"name": "dfslcxe",
"platform": "windows",
"package_type": "standalone",
"agent_version": "7.0.0.28644",
}
readable_output, outputs, _ = create_distribution_command(client, args)
expected_distribution_id = create_distribution_response.get("reply").get("distribution_id")
assert outputs == {
"CoreApiModule.Distribution(val.id == obj.id)": {
"id": expected_distribution_id,
"name": "dfslcxe",
"platform": "windows",
"package_type": "standalone",
"agent_version": "7.0.0.28644",
"description": None,
}
}
assert readable_output == f"Distribution {expected_distribution_id} created successfully"
def test_blocklist_files_command_with_more_than_one_file(requests_mock):
"""
Given:
- List of files' hashes to put in blocklist
When:
- A user desires to mark more than one file
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, blocklist_files_command
test_data = load_test_data("test_data/blocklist_allowlist_files_success.json")
expected_command_result = {
"CoreApiModule.blocklist.added_hashes.fileHash(val.fileHash == obj.fileHash)": test_data["multi_command_args"][
"hash_list"
]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/blocklist/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = blocklist_files_command(client, test_data["multi_command_args"])
assert expected_command_result == res.outputs
def test_blocklist_files_command_with_single_file(requests_mock):
"""
Given:
- List of a file hashes to put in blocklist.
When:
- A user desires to blocklist one file.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, blocklist_files_command
test_data = load_test_data("test_data/blocklist_allowlist_files_success.json")
expected_command_result = {
"CoreApiModule.blocklist.added_hashes.fileHash(val.fileHash == obj.fileHash)": test_data["single_command_args"][
"hash_list"
]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/blocklist/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = blocklist_files_command(client, test_data["single_command_args"])
assert expected_command_result == res.outputs
def test_blocklist_files_command_with_no_comment_file(requests_mock):
"""
Given:
- List of files' hashes to put in blocklist without passing the comment argument.
When:
- A user desires to blocklist files without adding a comment.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, blocklist_files_command
test_data = load_test_data("test_data/blocklist_allowlist_files_success.json")
expected_command_result = {
"CoreApiModule.blocklist.added_hashes.fileHash(val.fileHash == obj.fileHash)": test_data["no_comment_command_args"][
"hash_list"
]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/blocklist/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = blocklist_files_command(client, test_data["no_comment_command_args"])
assert expected_command_result == res.outputs
def test_allowlist_files_command_with_more_than_one_file(requests_mock):
"""
Given:
- List of files' hashes to put in allowlist
When:
- A user desires to mark more than one file
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, allowlist_files_command
test_data = load_test_data("test_data/blocklist_allowlist_files_success.json")
expected_command_result = {
"CoreApiModule.allowlist.added_hashes.fileHash(val.fileHash == obj.fileHash)": test_data["multi_command_args"][
"hash_list"
]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/allowlist/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = allowlist_files_command(client, test_data["multi_command_args"])
assert expected_command_result == res.outputs
def test_allowlist_files_command_with_single_file(requests_mock):
"""
Given:
- List of a file hashes to put in allowlist.
When:
- A user desires to allowlist one file.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, allowlist_files_command
test_data = load_test_data("test_data/blocklist_allowlist_files_success.json")
expected_command_result = {
"CoreApiModule.allowlist.added_hashes.fileHash(val.fileHash == obj.fileHash)": test_data["single_command_args"][
"hash_list"
]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/allowlist/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = allowlist_files_command(client, test_data["single_command_args"])
assert expected_command_result == res.outputs
def test_allowlist_files_command_with_no_comment_file(requests_mock):
"""
Given:
- List of files' hashes to put in allowlist without passing the comment argument.
When:
- A user desires to allowlist files without adding a comment.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, allowlist_files_command
test_data = load_test_data("test_data/blocklist_allowlist_files_success.json")
expected_command_result = {
"CoreApiModule.allowlist.added_hashes.fileHash(val.fileHash == obj.fileHash)": test_data["no_comment_command_args"][
"hash_list"
]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/allowlist/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = allowlist_files_command(client, test_data["no_comment_command_args"])
assert expected_command_result == res.outputs
def test_quarantine_files_command(requests_mock):
"""
Given:
- List of files' hashes to put in quarantine
When:
- A user desires to quarantine files.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, quarantine_files_command
test_data = load_test_data("test_data/quarantine_files.json")
quarantine_files_expected_tesult = {
"CoreApiModule.quarantineFiles.actionIds(val.actionId === obj.actionId)": test_data["context_data"]
}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/quarantine/",
json=test_data["api_response"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = quarantine_files_command(client, test_data["command_args"])
assert quarantine_files_expected_tesult == res.outputs
def test_get_quarantine_status_command(requests_mock):
"""
Given:
- Endpoint_id, file_path, file_hash
When:
- A user desires to check a file's quarantine status.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, get_quarantine_status_command
test_data = load_test_data("test_data/get_quarantine_status.json")
quarantine_files_expected_tesult = {
"CoreApiModule.quarantineFiles.status(val.fileHash === obj.fileHash &&val.endpointId"
" === obj.endpointId && val.filePath === obj.filePath)": test_data["context_data"]
}
requests_mock.post(f"{Core_URL}/public_api/v1/quarantine/status/", json=test_data["api_response"])
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = get_quarantine_status_command(client, test_data["command_args"])
assert quarantine_files_expected_tesult == res.outputs
def test_restore_file_command(requests_mock):
"""
Given:
- file_hash
When:
- A user desires to restore a file.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, restore_file_command
restore_expected_tesult = {"CoreApiModule.restoredFiles.actionId(val.actionId == obj.actionId)": 123}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/restore/",
json={"reply": {"action_id": 123}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = restore_file_command(client, {"file_hash": "123"})
assert restore_expected_tesult == res.outputs
def test_endpoint_scan_command(requests_mock):
"""
Given:
- endpoint_id_list, dist_name, gte_first_seen, gte_last_seen, lte_first_seen, lte_last_seen, ip_list,
group_name, platform, alias, isolate, hostname
When:
- A user desires to scan endpoint.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, endpoint_scan_command
test_data = load_test_data("test_data/scan_endpoints.json")
scan_expected_tesult = {
"CoreApiModule.endpointScan(val.actionId == obj.actionId)": {
"actionId": 123,
"aborted": False,
}
}
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/scan/", json={"reply": {"action_id": 123}})
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = endpoint_scan_command(client, test_data["command_args"])
assert scan_expected_tesult == res.outputs
def test_endpoint_scan_command_scan_all_endpoints(requests_mock):
"""
Given:
- the filter all as true.
When:
- A user desires to scan all endpoints.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, endpoint_scan_command
test_data = load_test_data("test_data/scan_all_endpoints.json")
scan_expected_tesult = {
"CoreApiModule.endpointScan(val.actionId == obj.actionId)": {
"actionId": 123,
"aborted": False,
}
}
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/scan/", json={"reply": {"action_id": 123}})
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = endpoint_scan_command(client, test_data["command_args"])
assert scan_expected_tesult == res.outputs
def test_endpoint_scan_command_scan_all_endpoints_no_filters_error(requests_mock):
"""
Given:
- No filters.
When:
- A user desires to scan all endpoints but without the correct argumetns.
Then:
- raise a descriptive error.
"""
from CoreIRApiModule import CoreClient, endpoint_scan_command
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/scan/", json={"reply": {"action_id": 123}})
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
err_msg = (
"To scan/abort scan all the endpoints run this command with the 'all' argument as True "
"and without any other filters. This may cause performance issues.\n"
"To scan/abort scan some of the endpoints, please use the filter arguments."
)
with pytest.raises(Exception, match=err_msg):
endpoint_scan_command(client, {})
def test_endpoint_scan_abort_command_scan_all_endpoints_no_filters_error(requests_mock):
"""
Given:
- No filters.
When:
- A user desires to abort scan on all endpoints but without the correct arguments.
Then:
- raise a descriptive error.
"""
from CoreIRApiModule import CoreClient, endpoint_scan_abort_command
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/abort_scan/",
json={"reply": {"action_id": 123}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
err_msg = (
"To scan/abort scan all the endpoints run this command with the 'all' argument as True "
"and without any other filters. This may cause performance issues.\n"
"To scan/abort scan some of the endpoints, please use the filter arguments."
)
with pytest.raises(Exception, match=err_msg):
endpoint_scan_abort_command(client, {})
def test_endpoint_scan_abort_command(requests_mock):
"""
Given:
- endpoint_id_list, dist_name, gte_first_seen, gte_last_seen, lte_first_seen, lte_last_seen, ip_list,
group_name, platform, alias, isolate, hostname
When:
- A user desires to abort scan endpoint.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, endpoint_scan_abort_command
test_data = load_test_data("test_data/scan_endpoints.json")
scan_expected_tesult = {
"CoreApiModule.endpointScan(val.actionId == obj.actionId)": {
"actionId": 123,
"aborted": True,
}
}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/abort_scan/",
json={"reply": {"action_id": 123}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = endpoint_scan_abort_command(client, test_data["command_args"])
assert scan_expected_tesult == res.outputs
def test_endpoint_scan_abort_command_all_endpoints(requests_mock):
"""
Given:
- the filter all as true.
When:
- A user desires to abort scan for all endpoints.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, endpoint_scan_abort_command
test_data = load_test_data("test_data/scan_all_endpoints.json")
scan_expected_tesult = {
"CoreApiModule.endpointScan(val.actionId == obj.actionId)": {
"actionId": 123,
"aborted": True,
}
}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/abort_scan/",
json={"reply": {"action_id": 123}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
client._headers = {}
res = endpoint_scan_abort_command(client, test_data["command_args"])
assert scan_expected_tesult == res.outputs
def test_get_update_args_unassgning_user(mocker):
"""
Given:
- a dict indicating changed fields (delta) with assigned_user_mail set to "None"
- the incident status - set to 1 == Active
When:
- running get_update_args
Then:
- update_args have assigned_user_mail and assigned_user_pretty_name set to None and unassign_user set to 'true'
"""
from CommonServerPython import UpdateRemoteSystemArgs
from CoreIRApiModule import get_update_args
mocker.patch("CoreIRApiModule.handle_outgoing_issue_closure")
remote_args = UpdateRemoteSystemArgs({"delta": {"assigned_user_mail": "None"}})
update_args = get_update_args(remote_args)
assert update_args.get("assigned_user_mail") is None
assert update_args.get("assigned_user_pretty_name") is None
assert update_args.get("unassign_user") == "true"
def test_handle_outgoing_issue_closure_close_reason(mocker):
"""
Given:
- a dict indicating changed fields (delta)
- the incident status - set to set to 2 == Closed
When:
- running handle_outgoing_issue_closure
Then:
- Closing the issue with the resolved_security_testing status
"""
from CommonServerPython import UpdateRemoteSystemArgs
from CoreIRApiModule import handle_outgoing_issue_closure
remote_args = UpdateRemoteSystemArgs(
{
"delta": {"assigned_user_mail": "None", "closeReason": "Security Testing"},
"status": 2,
"inc_status": 2,
"data": {"status": "other"},
}
)
request_data_log = mocker.patch.object(demisto, "debug")
handle_outgoing_issue_closure(remote_args)
assert (
"handle_outgoing_issue_closure Closing Remote incident ID: None with status resolved_security_testing"
in request_data_log.call_args[ # noqa: E501
0
][0]
)
def test_get_update_args_close_incident():
"""
Given:
- a dict indicating changed fields (delta) with closeReason set to Other and a closeNotes
- the incident status - set to 2 == Closed
- the current status of the remote incident are 'new'
When:
- running get_update_args
Then:
- update_args status has the correct status (resolved_other)
- the resolve_comment is the same as the closeNotes
"""
from CommonServerPython import UpdateRemoteSystemArgs
from CoreIRApiModule import get_update_args
remote_args = UpdateRemoteSystemArgs(
{
"delta": {
"closeReason": "Other",
"closeNotes": "Not Relevant",
"closingUserId": "admin",
},
"data": {"status": "new"},
"status": 2,
}
)
update_args = get_update_args(remote_args)
assert update_args.get("status") == "resolved_other"
assert update_args.get("resolve_comment") == "Not Relevant"
def test_get_update_args_owner_sync(mocker):
"""
Given:
- a dict indicating changed fields (delta) with a change in owner
- the incident status - set to 2 == Close
When:
- running get_update_args
Then:
- update_args assigned_user_mail has the correct associated mail
"""
from CommonServerPython import UpdateRemoteSystemArgs
from CoreIRApiModule import get_update_args
remote_args = UpdateRemoteSystemArgs({"delta": {"owner": "username"}, "data": {"status": "new"}})
mocker.patch.object(
demisto,
"params",
return_value={"sync_owners": True, "mirror_direction": "Incoming"},
)
mocker.patch.object(
demisto,
"findUser",
return_value={"email": "moo@demisto.com", "username": "username"},
)
update_args = get_update_args(remote_args)
assert update_args.get("assigned_user_mail") == "moo@demisto.com"
def test_get_policy(requests_mock):
"""
Given:
-endpoint_id
When:
-Retrieving the policy name of the requested actions according to the specific endpoint.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CoreIRApiModule import CoreClient, get_policy_command
expected_context = {
"endpoint_id": "aeec6a2cc92e46fab3b6f621722e9916",
"policy_name": "test",
}
run_script_expected_result = {"CoreApiModule.Policy(val.endpoint_id == obj.endpoint_id)": expected_context}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/get_policy/",
json={"reply": {"policy_name": "test"}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"endpoint_id": "aeec6a2cc92e46fab3b6f621722e9916"}
hr, context, raw_response = get_policy_command(client, args)
assert hr == "The policy name of endpoint: aeec6a2cc92e46fab3b6f621722e9916 is: test."
assert run_script_expected_result == context
assert raw_response == {"policy_name": "test"}
def test_get_endpoint_device_control_violations_command(requests_mock):
"""
Given:
- violation_id_list='100'
When:
- Request for list of device control violations filtered by selected fields.
You can retrieve up to 100 violations.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CommonServerPython import (
string_to_table_header,
tableToMarkdown,
timestamp_to_datestring,
)
from CoreIRApiModule import (
CoreClient,
get_endpoint_device_control_violations_command,
)
get_endpoint_violations_reply = load_test_data("./test_data/get_endpoint_violations.json")
violations = get_endpoint_violations_reply.get("reply").get("violations")
for violation in violations:
timestamp = violation.get("timestamp")
violation["date"] = timestamp_to_datestring(timestamp, "%Y-%m-%dT%H:%M:%S")
get_endpoint_violations_expected_result = {"CoreApiModule.EndpointViolations(val.violation_id==obj.violation_id)": violations}
headers = [
"date",
"hostname",
"platform",
"username",
"ip",
"type",
"violation_id",
"vendor",
"product",
"serial",
]
requests_mock.post(
f"{Core_URL}/public_api/v1/device_control/get_violations/",
json=get_endpoint_violations_reply,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"violation_id_list": "100"}
hr, context, raw_response = get_endpoint_device_control_violations_command(client, args)
assert hr == tableToMarkdown(
name="Endpoint Device Control Violation",
t=violations,
headers=headers,
headerTransform=string_to_table_header,
removeNull=True,
)
assert context == get_endpoint_violations_expected_result
assert raw_response == get_endpoint_violations_reply.get("reply")
def test_retrieve_files_command(requests_mock):
"""
Given:
- endpoint_ids
- windows_file_paths
When:
- A user desires to retrieve a file.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CommonServerPython import string_to_table_header, tableToMarkdown
from CoreIRApiModule import CoreClient, retrieve_files_command
retrieve_expected_result = {"action_id": 1773}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/file_retrieval/",
json={"reply": {"action_id": 1773}},
)
result = {"action_id": 1773}
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
res = retrieve_files_command(
client,
{
"endpoint_ids": "aeec6a2cc92e46fab3b6f621722e9916",
"windows_file_paths": "C:\\Users\\demisto\\Desktop\\demisto.txt",
},
)
assert res.readable_output == tableToMarkdown(name="Retrieve files", t=result, headerTransform=string_to_table_header)
assert res.outputs == retrieve_expected_result
assert res.raw_response == {"action_id": 1773}
def test_retrieve_files_command_with_custom_paths_separator(requests_mock):
"""
Given:
- endpoint_ids
- windows_file_paths containing a literal comma in the path
- paths_separator set to ";"
When:
- A user desires to retrieve a file whose path contains a comma.
Then:
- Assert the path is sent as a single entry (not split on the comma).
"""
from CoreIRApiModule import CoreClient, retrieve_files_command
mock_request = requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/file_retrieval/",
json={"reply": {"action_id": 1773}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
retrieve_files_command(
client,
{
"endpoint_ids": "aeec6a2cc92e46fab3b6f621722e9916",
"windows_file_paths": "C:\\Users\\TrangLe\\Downloads\\test, test\\test.txt",
"paths_separator": ";",
},
)
sent_files = mock_request.last_request.json()["request_data"]["files"]
assert sent_files["windows"] == ["C:\\Users\\TrangLe\\Downloads\\test, test\\test.txt"]
def test_retrieve_files_command_default_separator_splits_on_comma(requests_mock):
"""
Given:
- endpoint_ids
- windows_file_paths with two comma-separated paths and no paths_separator
When:
- A user retrieves files using the default (comma) separator.
Then:
- Assert backward compatibility: the value is split into two paths on the comma.
"""
from CoreIRApiModule import CoreClient, retrieve_files_command
mock_request = requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/file_retrieval/",
json={"reply": {"action_id": 1773}},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
retrieve_files_command(
client,
{
"endpoint_ids": "aeec6a2cc92e46fab3b6f621722e9916",
"windows_file_paths": "C:\\path\\one.txt,C:\\path\\two.txt",
},
)
sent_files = mock_request.last_request.json()["request_data"]["files"]
assert sent_files["windows"] == ["C:\\path\\one.txt", "C:\\path\\two.txt"]
def test_retrieve_files_command_using_general_file_path(requests_mock):
"""
Given:
- endpoint_ids
- generic_file_path
When:
- A user desires to retrieve a file.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CommonServerPython import string_to_table_header, tableToMarkdown
from CoreIRApiModule import CoreClient, retrieve_files_command
retrieve_expected_result = {"action_id": 1773}
requests_mock.post(
f"{Core_URL}/public_api/v1/endpoints/file_retrieval/",
json={"reply": {"action_id": 1773}},
)
result = {"action_id": 1773}
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoint/", json=get_endpoints_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
res = retrieve_files_command(
client,
{
"endpoint_ids": "aeec6a2cc92e46fab3b6f621722e9916",
"generic_file_path": "C:\\Users\\demisto\\Desktop\\demisto.txt",
},
)
assert res.readable_output == tableToMarkdown(name="Retrieve files", t=result, headerTransform=string_to_table_header)
assert res.outputs == retrieve_expected_result
assert res.raw_response == {"action_id": 1773}
def test_retrieve_files_command_using_general_file_path_without_valid_endpint(
requests_mock,
):
"""
Given:
- endpoint_ids
- generic_file_path
When:
- A user desires to retrieve a file.
- The endpoint is invalid
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CoreIRApiModule import CoreClient, retrieve_files_command
get_endpoints_response = {"reply": {"result_count": 1, "endpoints": []}}
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoint/", json=get_endpoints_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
with pytest.raises(ValueError) as error:
retrieve_files_command(
client,
{
"endpoint_ids": "aeec6a2cc92e46fab3b6f621722e9916",
"generic_file_path": "C:\\Users\\demisto\\Desktop\\demisto.txt",
},
)
assert str(error.value) == "Error: Endpoint aeec6a2cc92e46fab3b6f621722e9916 was not found"
def test_retrieve_file_details_command(requests_mock):
"""
Given:
- action_id
When:
- Requesting to view the file retrieved by the Retrieve File request according to the action ID.
Then:
- Assert the returned markdown, file result are as expected.
"""
from CoreIRApiModule import CoreClient, retrieve_file_details_command
data = load_test_data("./test_data/retrieve_file_details.json")
data1 = "test_file"
retrieve_expected_hr = {
"Type": 1,
"ContentsFormat": "json",
"Contents": [data.get("reply").get("data")],
"HumanReadable": "### Action id : 1788 \n Retrieved 1 files from 1 endpoints. \n "
"To get the exact action status run the core-action-status-get command",
"ReadableContentsFormat": "markdown",
"EntryContext": {},
}
requests_mock.post(f"{Core_URL}/public_api/v1/actions/file_retrieval_details/", json=data)
requests_mock.get(f"{Core_URL}/public_api/v1/download/file_hash", json=data1)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"action_id": "1788"}
results, file_result = retrieve_file_details_command(client, args, False)
assert results == retrieve_expected_hr
assert file_result[0]["File"] == "endpoint_test_1.zip"
def test_get_scripts_command(requests_mock):
"""
Given:
- script_name
When:
- Requesting for a list of scripts available in the scripts library.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CommonServerPython import (
string_to_table_header,
tableToMarkdown,
timestamp_to_datestring,
)
from CoreIRApiModule import CoreClient, get_scripts_command
get_scripts_response = load_test_data("./test_data/get_scripts.json")
scripts = copy.deepcopy(get_scripts_response.get("reply").get("scripts")[0::50])
for script in scripts:
timestamp = script.get("modification_date")
script["modification_date_timestamp"] = timestamp
script["modification_date"] = timestamp_to_datestring(timestamp, "%Y-%m-%dT%H:%M:%S")
headers: list = [
"name",
"description",
"script_uid",
"modification_date",
"created_by",
"windows_supported",
"linux_supported",
"macos_supported",
"is_high_risk",
]
get_scripts_expected_result = {"CoreApiModule.Scripts(val.script_uid == obj.script_uid)": scripts}
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/get_scripts/", json=get_scripts_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"script_name": "process_get"}
hr, context, raw_response = get_scripts_command(client, args)
assert hr == tableToMarkdown(
name="Scripts",
t=scripts,
headers=headers,
removeNull=True,
headerTransform=string_to_table_header,
)
assert context == get_scripts_expected_result
assert raw_response == get_scripts_response.get("reply")
def test_get_script_metadata_command(requests_mock):
"""
Given:
- A script_uid
When:
- Requesting for a given script metadata.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CommonServerPython import (
string_to_table_header,
tableToMarkdown,
timestamp_to_datestring,
)
from CoreIRApiModule import CoreClient, get_script_metadata_command
get_script_metadata_response = load_test_data("./test_data/get_script_metadata.json")
get_scripts_expected_result = {
"CoreApiModule.ScriptMetadata(val.script_uid == obj.script_uid)": get_script_metadata_response.get("reply")
}
script_metadata = copy.deepcopy(get_script_metadata_response).get("reply")
timestamp = script_metadata.get("modification_date")
script_metadata["modification_date_timestamp"] = timestamp
script_metadata["modification_date"] = timestamp_to_datestring(timestamp, "%Y-%m-%dT%H:%M:%S")
requests_mock.post(
f"{Core_URL}/public_api/v1/scripts/get_script_metadata/",
json=get_script_metadata_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"script_uid": "956e8989f67ebcb2c71c4635311e47e4"}
hr, context, raw_response = get_script_metadata_command(client, args)
assert hr == tableToMarkdown(
name="Script Metadata",
t=script_metadata,
removeNull=True,
headerTransform=string_to_table_header,
)
assert context == get_scripts_expected_result
assert raw_response == get_script_metadata_response.get("reply")
def test_get_script_code_command(requests_mock):
"""
Given:
- A script_uid.
When:
- Requesting the code of a specific script in the script library.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CoreIRApiModule import CoreClient, get_script_code_command
get_script_code_command_reply = load_test_data("./test_data/get_script_code.json")
context = {
"script_uid": "548023b6e4a01ec51a495ba6e5d2a15d",
"code": get_script_code_command_reply.get("reply"),
}
get_script_code_command_expected_result = {"CoreApiModule.ScriptCode(val.script_uid == obj.script_uid)": context}
requests_mock.post(
f"{Core_URL}/public_api/v1/scripts/get_script_code/",
json=get_script_code_command_reply,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"script_uid": "548023b6e4a01ec51a495ba6e5d2a15d"}
hr, context, raw_response = get_script_code_command(client, args)
assert hr == f"### Script code: \n ``` {get_script_code_command_reply.get('reply')!s} ```"
assert context == get_script_code_command_expected_result
assert raw_response == get_script_code_command_reply.get("reply")
def test_action_status_get_command(mocker):
"""
Given:
- An action_id
When:
- Retrieving the status of the requested actions according to the action ID.
Then:
- Assert the returned markdown, context data and raw response are as expected.
"""
from CommonServerPython import tableToMarkdown
from CoreIRApiModule import CoreClient, action_status_get_command
action_status_get_command_command_reply = load_test_data("./test_data/action_status_get.json")
data = action_status_get_command_command_reply.get("reply").get("data")
error_reasons = action_status_get_command_command_reply.get("reply").get("errorReasons") or {}
result = []
for item in data:
result.append({"action_id": 1810, "endpoint_id": item, "status": data.get(item)})
if error_reason := error_reasons.get(item):
result[-1]["error_description"] = error_reason["errorDescription"]
result[-1]["ErrorReasons"] = error_reason
action_status_get_command_expected_result = result
mocker.patch.object(
CoreClient,
"_http_request",
return_value=action_status_get_command_command_reply,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"action_id": "1810"}
res = action_status_get_command(client, args)
assert res.readable_output == tableToMarkdown(
name="Get Action Status",
t=result,
removeNull=True,
headers=["action_id", "endpoint_id", "status", "error_description"],
)
assert res.outputs == action_status_get_command_expected_result
assert res.raw_response == result
def test_sort_by_key__only_main_key():
"""
Given:
- a list of dicts to sort where main key is entered for all elements
- the main key to sort by
- the fallback key to sort by
When:
- running sort_by_key
Then:
- resulting list is sorted by main key only.
"""
from CoreIRApiModule import sort_by_key
list_to_sort = [
{"name": "element2", "main_key": 2, "fallback_key": 4},
{"name": "element1", "main_key": 1, "fallback_key": 3},
{"name": "element4", "main_key": 4, "fallback_key": 2},
{"name": "element3", "main_key": 3, "fallback_key": 1},
]
expected_result = [
{"name": "element1", "main_key": 1, "fallback_key": 3},
{"name": "element2", "main_key": 2, "fallback_key": 4},
{"name": "element3", "main_key": 3, "fallback_key": 1},
{"name": "element4", "main_key": 4, "fallback_key": 2},
]
assert expected_result == sort_by_key(list_to_sort, "main_key", "fallback_key")
def test_sort_by_key__main_key_and_fallback_key():
"""
Given:
- a list of dicts to sort where some elements have main key and some don't but they have fallback key
- the main key to sort by
- the fallback key to sort by
When:
- running sort_by_key
Then:
- resulting list is sorted by main key on elements with the main key and
Then: sorted by fallback key for elements who dont have it
"""
from CoreIRApiModule import sort_by_key
list_to_sort = [
{"name": "element2", "fallback_key": 4},
{"name": "element1", "main_key": 1, "fallback_key": 3},
{"name": "element4", "main_key": None, "fallback_key": 2},
{"name": "element3", "main_key": 3, "fallback_key": 1},
]
expected_result = [
{"name": "element1", "main_key": 1, "fallback_key": 3},
{"name": "element3", "main_key": 3, "fallback_key": 1},
{"name": "element4", "main_key": None, "fallback_key": 2},
{"name": "element2", "fallback_key": 4},
]
assert expected_result == sort_by_key(list_to_sort, "main_key", "fallback_key")
def test_sort_by_key__only_fallback_key():
"""
Given:
- a list of dicts to sort where main key is not entered for all elements and fallback key is.
- the main key to sort by
- the fallback key to sort by
When:
- running sort_by_key
Then:
- resulting list is sorted by fallback key only.
"""
from CoreIRApiModule import sort_by_key
list_to_sort = [
{"name": "element2", "fallback_key": 4},
{"name": "element1", "fallback_key": 3},
{"name": "element4", "fallback_key": 2},
{"name": "element3", "fallback_key": 1},
]
expected_result = [
{"name": "element3", "fallback_key": 1},
{"name": "element4", "fallback_key": 2},
{"name": "element1", "fallback_key": 3},
{"name": "element2", "fallback_key": 4},
]
assert expected_result == sort_by_key(list_to_sort, "main_key", "fallback_key")
def test_sort_by_key__main_key_and_fallback_key_and_additional():
"""
Given:
- a list of dicts to sort where main key is entered for some elements, fallback for others
and some dont have either
- the main key to sort by
- the fallback key to sort by
When:
- running sort_by_key
Then:
- resulting list is sorted by main key for elements with main key,
Then: by fallback key for those with fallback key and Then: the rest of the elements that dont have either key.
"""
from CoreIRApiModule import sort_by_key
list_to_sort = [
{"name": "element2", "fallback_key": 4},
{"name": "element1", "main_key": 1, "fallback_key": 3},
{"name": "element4", "main_key": None, "fallback_key": None},
{"name": "element3", "main_key": 3, "fallback_key": 1},
]
expected_result = [
{"name": "element1", "main_key": 1, "fallback_key": 3},
{"name": "element3", "main_key": 3, "fallback_key": 1},
{"name": "element2", "fallback_key": 4},
{"name": "element4", "main_key": None, "fallback_key": None},
]
assert expected_result == sort_by_key(list_to_sort, "main_key", "fallback_key")
def test_create_account_context_with_data():
"""
Given:
- get_endpoints command
When:
- creating the account context from the response succeeds - which means there exists both domain
and user in the response.
Then:
- verify the context is created successfully.
"""
from CoreIRApiModule import create_account_context
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
endpoints_list = get_endpoints_response.get("reply").get("endpoints")
endpoints_list[0]["domain"] = "test.domain"
account_context = create_account_context(endpoints_list)
assert account_context == [{"Username": "ec2-user", "Domain": "test.domain"}]
def test_create_account_context_no_domain():
"""
Given:
- get_endpoints command
When:
- the endpoint is missing a domain - which means an account context can't be created.
Then:
- verify the account context is an empty list and the method is finished with no errors.
"""
from CoreIRApiModule import create_account_context
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
endpoints_list = get_endpoints_response.get("reply").get("endpoints")
account_context = create_account_context(endpoints_list)
assert account_context == []
def test_create_account_context_user_is_none():
"""
Given:
- get_endpoints command
When:
- the user value is None - which means an account context can't be created.
Then:
- verify the account context is an empty list and the method is finished with no errors.
"""
from CoreIRApiModule import create_account_context
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
endpoints_list = get_endpoints_response.get("reply").get("endpoints")
endpoints_list[0]["user"] = None
account_context = create_account_context(endpoints_list)
assert account_context == []
def test_run_script_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, script UID, script parameters and incident ID
When:
- Running run-script command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
script_uid = "script_uid"
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
parameters = '{"param1":"value1","param2":2}'
args = {
"script_uid": script_uid,
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"parameters": parameters,
"incident_id": "4",
}
response = run_script_command(client, args)
assert response.outputs == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": script_uid,
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": json.loads(parameters),
}
}
def test_run_script_command_empty_params(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, script UID, empty params and incident ID
When:
- Running run-script command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
script_uid = "script_uid"
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
parameters = ""
args = {
"script_uid": script_uid,
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"parameters": parameters,
"incident_id": "4",
}
response = run_script_command(client, args)
assert response.outputs == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": script_uid,
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {},
}
}
def test_run_snippet_code_script_command_no_incident_id(requests_mock):
"""
Given:
- Core client
- Endpoint IDs and snippet code
When:
- Running run-snippet-code-script command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_snippet_code_script_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_snippet_code_script", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
snippet_code = 'print("hello world")'
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
args = {
"snippet_code": snippet_code,
"endpoint_ids": endpoint_ids,
"timeout": timeout,
}
response = run_snippet_code_script_command(client, args)
assert response.outputs == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"snippet_code": snippet_code,
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
}
}
def test_run_snippet_code_script_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs snippet code and incident ID
When:
- Running run-snippet-code-script command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_snippet_code_script_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_snippet_code_script", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
snippet_code = 'print("hello world")'
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
args = {
"snippet_code": snippet_code,
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"incident_id": "4",
}
response = run_snippet_code_script_command(client, args)
assert response.outputs == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"snippet_code": snippet_code,
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
}
}
def test_get_script_execution_status_command(requests_mock):
"""
Given:
- Core client
- Action ID
When:
- Running get-script-execution-status command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, get_script_execution_status_command
api_response = load_test_data("./test_data/get_script_execution_status.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/scripts/get_script_execution_status/",
json=api_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
action_id = "1"
args = {"action_id": action_id}
response = get_script_execution_status_command(client, args)
api_response["reply"]["action_id"] = int(action_id)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {"request_data": {"action_id": action_id}}
def test_get_script_execution_results_command(requests_mock):
"""
Given:
- Core client
- Action ID
When:
- Running get-script-execution-results command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, get_script_execution_results_command
api_response = load_test_data("./test_data/get_script_execution_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/scripts/get_script_execution_results",
json=api_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
action_id = "1"
args = {"action_id": action_id}
response = get_script_execution_results_command(client, args)
expected_output = {
"action_id": int(action_id),
"results": api_response.get("reply").get("results"),
}
assert response[0].outputs == expected_output
assert requests_mock.request_history[0].json() == {"request_data": {"action_id": action_id}}
def test_get_script_execution_files_command(requests_mock, mocker, request):
"""
Given:
- Core client
- Action ID and endpoint ID
When:
- Running get-script-execution-files command
Then:
- Verify file name is extracted
- Verify output ZIP file contains text file
"""
from CoreIRApiModule import CoreClient, get_script_execution_result_files_command
mocker.patch.object(demisto, "uniqueFile", return_value="test_file_result")
mocker.patch.object(demisto, "investigation", return_value={"id": "1"})
file_name = "1_test_file_result"
def cleanup():
try:
os.remove(file_name)
except OSError:
pass
request.addfinalizer(cleanup)
zip_link = "https://download/example-link"
zip_filename = "file.zip"
requests_mock.post(
f"{Core_URL}/public_api/v1/scripts/get_script_execution_results_files",
json={"reply": {"DATA": zip_link}},
)
requests_mock.get(
f"{Core_URL}/public_api/v1/download/example-link",
content=b"PK\x03\x04\x14\x00\x00\x00\x00\x00%\x98>R\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x00\x00\x00\x00your_file"
b".txtPK\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00%\x98>R\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x00\x00\x00\x00your_file"
b".txtPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00;\x00\x00\x00+\x00\x00\x00\x00\x00",
headers={"Content-Disposition": f"attachment; filename={zip_filename}"},
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
action_id = "action_id"
endpoint_id = "endpoint_id"
args = {"action_id": action_id, "endpoint_id": endpoint_id}
response = get_script_execution_result_files_command(client, args)
assert response["File"] == zip_filename
assert zipfile.ZipFile(file_name).namelist() == ["your_file.txt"]
def test_get_script_execution_files_command_rbac(mocker, request):
"""
Given:
- An XSIAM tenant where FORWARD_USER_RUN_RBAC is True, so requests are forwarded
through demisto._apiCall and no requests.Response object is ever available.
When:
- Running the get-script-execution-result-files command.
Then:
- Verify the command does not fail and returns a valid ZIP file result.
"""
import base64
from CoreIRApiModule import CoreClient, get_script_execution_result_files_command
mocker.patch.object(demisto, "uniqueFile", return_value="test_rbac_file_result")
mocker.patch.object(demisto, "investigation", return_value={"id": "1"})
file_name = "1_test_rbac_file_result"
def cleanup():
try:
os.remove(file_name)
except OSError:
pass
request.addfinalizer(cleanup)
zip_bytes = (
b"PK\x03\x04\x14\x00\x00\x00\x00\x00%\x98>R\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x00\x00\x00\x00your_file"
b".txtPK\x01\x02\x14\x00\x14\x00\x00\x00\x00\x00%\x98>R\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb6\x81\x00\x00\x00\x00your_file"
b".txtPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00;\x00\x00\x00+\x00\x00\x00\x00\x00"
)
mocker.patch("CoreIRApiModule.FORWARD_USER_RUN_RBAC", new=True)
mocker.patch("CoreIRApiModule.ALLOW_RESPONSE_AS_BINARY", new=True)
mocker.patch.object(
demisto,
"_apiCall",
side_effect=[
{
"name": "/api/webapp/public_api/v1/scripts/get_script_execution_results_files",
"status": 200,
"data": json.dumps({"reply": {"DATA": "https://test.com/download/example-link.zip"}}),
},
{
"name": "/api/webapp/public_api/v1/download/example-link.zip",
"status": 200,
"data": base64.b64encode(zip_bytes),
},
],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"action_id": "action_id", "endpoint_id": "endpoint_id"}
response = get_script_execution_result_files_command(client, args)
assert response["File"] == "action_id.zip"
assert zipfile.ZipFile(file_name).namelist() == ["your_file.txt"]
@pytest.mark.parametrize("command_input, expected_command", POWERSHELL_COMMAND_CASES)
def test_form_powershell_command(command_input: str, expected_command: str):
"""
Given:
- An unescaped command containing characters like ', `, ", \
When:
- Calling the form_powershell_command function
Assert:
- Command starts with 'powershell -Command' and is properly escaped.
"""
from CoreIRApiModule import form_powershell_command
command = form_powershell_command(command_input)
assert not command_input.startswith("powershell -Command ")
assert command == expected_command
def test_run_script_execute_commands_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, shell commands and incident ID
When:
- Running run-script-execute-commands command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_execute_commands_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
commands = "echo hi"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"commands": commands,
"incident_id": "4",
}
response = run_script_execute_commands_command(client, args)
assert response.outputs == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "a6f7683c8e217d85bd3c398f0d3fb6bf",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"commands_list": commands.split(",")},
}
}
def test_run_script_delete_file_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, file path and incident ID
When:
- Running run-script-delete-file command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_delete_file_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
file_path = "my_file.txt"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"file_path": file_path,
"incident_id": "4",
}
response = run_script_delete_file_command(client, args)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "548023b6e4a01ec51a495ba6e5d2a15d",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"file_path": args.get("file_path")},
}
}
def test_run_script_delete_multiple_files_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, files paths and incident ID
When:
- Running run-script-delete-file command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_delete_file_command
api_response = load_test_data("./test_data/run_script_multiple_inputs_and_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
file_path = "my_file.txt,test.txt"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"file_path": file_path,
"incident_id": "4",
}
response = run_script_delete_file_command(client, args)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "548023b6e4a01ec51a495ba6e5d2a15d",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"file_path": "my_file.txt"},
}
}
assert requests_mock.request_history[1].json() == {
"request_data": {
"script_uid": "548023b6e4a01ec51a495ba6e5d2a15d",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"file_path": "test.txt"},
}
}
def test_run_script_file_exists_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, file path and incident ID
When:
- Running run-script-file-exists command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_file_exists_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
file_path = "my_file.txt"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"file_path": file_path,
"incident_id": "4",
}
response = run_script_file_exists_command(client, args)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "414763381b5bfb7b05796c9fe690df46",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"path": args.get("file_path")},
}
}
def test_run_script_file_exists_multiple_files_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, files paths and incident ID
When:
- Running run-script-file-exists command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_file_exists_command
api_response = load_test_data("./test_data/run_script_multiple_inputs_and_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
file_path = "my_file.txt,test.txt"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"file_path": file_path,
"incident_id": "4",
}
response = run_script_file_exists_command(client, args)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "414763381b5bfb7b05796c9fe690df46",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"path": "my_file.txt"},
}
}
assert requests_mock.request_history[1].json() == {
"request_data": {
"script_uid": "414763381b5bfb7b05796c9fe690df46",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"path": "test.txt"},
}
}
def test_run_script_kill_process_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, process name and incident ID
When:
- Running run-script-kill-process command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_kill_process_command
api_response = load_test_data("./test_data/run_script.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
process_name = "process.exe"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"process_name": process_name,
"incident_id": "4",
}
response = run_script_kill_process_command(client, args)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "fd0a544a99a9421222b4f57a11839481",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"process_name": process_name},
}
}
def test_run_script_kill_multiple_processes_command(requests_mock):
"""
Given:
- Core client
- Endpoint IDs, multiple processes names and incident ID
When:
- Running run-script-kill-process command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, run_script_kill_process_command
api_response = load_test_data("./test_data/run_script_multiple_inputs_and_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/scripts/run_script/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
endpoint_ids = "endpoint_id1,endpoint_id2"
timeout = "10"
processes_names = "process1.exe,process2.exe"
args = {
"endpoint_ids": endpoint_ids,
"timeout": timeout,
"process_name": processes_names,
"incident_id": "4",
}
response = run_script_kill_process_command(client, args)
assert response.outputs[0] == api_response.get("reply")
assert requests_mock.request_history[0].json() == {
"request_data": {
"script_uid": "fd0a544a99a9421222b4f57a11839481",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"process_name": "process1.exe"},
}
}
assert requests_mock.request_history[1].json() == {
"request_data": {
"script_uid": "fd0a544a99a9421222b4f57a11839481",
"timeout": int(timeout),
"filters": [
{
"field": "endpoint_id_list",
"operator": "in",
"value": endpoint_ids.split(","),
}
],
"incident_id": 4,
"parameters_values": {"process_name": "process2.exe"},
}
}
CONNECTED_STATUS = {
"endpoint_status": "Connected",
"is_isolated": "Isolated",
"host_name": "TEST",
"ip": "1.1.1.1",
}
NO_STATUS = {"is_isolated": "Isolated", "host_name": "TEST", "ip": "1.1.1.1"}
OFFLINE_STATUS = {
"endpoint_status": "Offline",
"is_isolated": "Isolated",
"host_name": "TEST",
"ip": "1.1.1.1",
}
PUBLIC_IP = {
"endpoint_status": "Connected",
"is_isolated": "Isolated",
"host_name": "TEST",
"ip": [],
"public_ip": ["1.1.1.1"],
}
NO_IP = {
"endpoint_status": "Connected",
"is_isolated": "Isolated",
"host_name": "TEST",
"ip": [],
"public_ip": [],
}
@pytest.mark.parametrize(
"endpoint, expected_status, expected_ip",
[
(CONNECTED_STATUS, "Online", "1.1.1.1"),
(NO_STATUS, "Offline", "1.1.1.1"),
(OFFLINE_STATUS, "Offline", "1.1.1.1"),
(PUBLIC_IP, "Online", ["1.1.1.1"]),
(NO_IP, "Online", ""),
],
)
def test_get_endpoint_properties(endpoint, expected_status, expected_ip):
"""
Given:
- Endpoint data
When:
- Case a: The status of the endpoint is 'Connected' with a capital C and ip is 1.1.1.1.
- Case b: When: no status is not given and ip is 1.1.1.1.
- Case c: The status of the endpoint is offline and ip is 1.1.1.1.
- Case d: The status of the endpoint is 'Connected' with a capital C ip is empty but public_ip is 1.1.1.1.
- Case d: The status of the endpoint is 'Connected' with a capital C and both ip and public_ip are empty.
Then:
- Case a: The status of the endpoint is determined to be 'Online' and the ip is set to 1.1.1.1.
- Case b: The status of the endpoint is determined to be 'Offline' and the ip is set to 1.1.1.1.
- Case c: The status of the endpoint is determined to be 'Offline' and the ip is set to 1.1.1.1.
- Case d: The status of the endpoint is determined to be 'Online' and the ip is set to 1.1.1.1.
- Case d: The status of the endpoint is determined to be 'Online' and the ip is set to empty.
"""
from CoreIRApiModule import get_endpoint_properties
status, is_isolated, hostname, ip = get_endpoint_properties(endpoint)
assert status == expected_status
assert ip == expected_ip
def test_remove_blocklist_files_command(requests_mock):
"""
Given:
- List of files' hashes to remove from blocklist.
When:
- A user desires to remove blocklist files.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, remove_blocklist_files_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
remove_blocklist_files_response = load_test_data("./test_data/remove_blocklist_files.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/blocklist/remove/",
json=remove_blocklist_files_response,
)
hash_list = [
"11d69fb388ff59e5ba6ca217ca04ecde6a38fa8fb306aa5f1b72e22bb7c3a25b",
"e5ab4d81607668baf7d196ae65c9cf56dd138e3fe74c4bace4765324a9e1c565",
]
res = remove_blocklist_files_command(client=client, args={"hash_list": hash_list, "comment": "", "incident_id": 606})
markdown_data = [{"removed_hashes": file_hash} for file_hash in hash_list]
assert res.readable_output == tableToMarkdown(
"Blocklist Files Removed",
markdown_data,
headers=["removed_hashes"],
headerTransform=pascalToSpace,
)
def test_blocklist_files_command_with_detailed_response(requests_mock):
"""
Given:
- List of files' hashes to add in blocklist with detailed_response.
When:
- A user desires to blocklist files with detailed_response.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, blocklist_files_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
blocklist_files_response = load_test_data("./test_data/add_blocklist_files_detailed_response.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/blocklist/",
json=blocklist_files_response,
)
hash_list = [
"11d69fb388ff59e5ba6ca217ca04ecde6a38fa8fb306aa5f1b72e22bb7c3a25b",
"e5ab4d81607668baf7d196ae65c9cf56dd138e3fe74c4bace4765324a9e1c565",
]
res = blocklist_files_command(
client=client,
args={
"hash_list": hash_list,
"comment": "",
"incident_id": 606,
"detailed_response": "true",
},
)
assert res.readable_output == tableToMarkdown("Blocklist Files", res.raw_response)
def test_remove_allowlist_files_command(requests_mock):
"""
Given:
- List of files' hashes to remove from allowlist.
When:
- A user desires to remove allowlist files.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, remove_allowlist_files_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
remove_allowlist_files_response = load_test_data("./test_data/remove_blocklist_files.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/allowlist/remove/",
json=remove_allowlist_files_response,
)
hash_list = [
"11d69fb388ff59e5ba6ca217ca04ecde6a38fa8fb306aa5f1b72e22bb7c3a25b",
"e5ab4d81607668baf7d196ae65c9cf56dd138e3fe74c4bace4765324a9e1c565",
]
res = remove_allowlist_files_command(client=client, args={"hash_list": hash_list, "comment": "", "incident_id": 606})
markdown_data = [{"removed_hashes": file_hash} for file_hash in hash_list]
assert res.readable_output == tableToMarkdown(
"Allowlist Files Removed",
markdown_data,
headers=["removed_hashes"],
headerTransform=pascalToSpace,
)
def test_allowlist_files_command_with_detailed_response(requests_mock):
"""
Given:
- List of files' hashes to add in allowlist with detailed_response.
When:
- A user desires to allowlist files with detailed_response.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, allowlist_files_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
allowlist_files_response = load_test_data("./test_data/add_blocklist_files_detailed_response.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/hash_exceptions/allowlist/",
json=allowlist_files_response,
)
hash_list = [
"11d69fb388ff59e5ba6ca217ca04ecde6a38fa8fb306aa5f1b72e22bb7c3a25b",
"e5ab4d81607668baf7d196ae65c9cf56dd138e3fe74c4bace4765324a9e1c565",
]
res = allowlist_files_command(
client=client,
args={
"hash_list": hash_list,
"comment": "",
"incident_id": 606,
"detailed_response": "true",
},
)
assert res.readable_output == tableToMarkdown("Allowlist Files", res.raw_response)
def test_decode_dict_values():
"""
Given:
- a dict to decode
When:
- Running decode_dict_values command
Then:
- Verify expected output
"""
from CoreIRApiModule import decode_dict_values
test_dict: dict = {
"x": 1,
"y": "test",
"z": '{"a": "test1", "b": "test2"}',
"w": {"t": '{"a": "test1", "b": "test2"}', "m": "test3"},
}
decode_dict_values(test_dict)
assert test_dict == {
"x": 1,
"y": "test",
"z": {"a": "test1", "b": "test2"},
"w": {"t": {"a": "test1", "b": "test2"}, "m": "test3"},
}
def test_filter_vendor_fields():
"""
Given:
- An alert dict to filter
When:
- Running test_filter_vendor_fields command
Then:
- Verify that the vendor fields were filtered properly
"""
from CoreIRApiModule import filter_vendor_fields
alert = {
"x": 1,
"event": {
"vendor": "Amazon",
"raw_log": {
"eventSource": "test1",
"requestID": "test2",
"should_be_filter": "N",
},
},
}
filter_vendor_fields(alert)
assert alert == {
"x": 1,
"event": {
"vendor": "Amazon",
"raw_log": {
"eventSource": "test1",
"requestID": "test2",
},
},
}
def test_filter_general_fields():
"""
Given:
- An alert dict
When:
- Running filter_general_fields command
Then:
- Verify expected output
"""
from CoreIRApiModule import filter_general_fields
alert = {
"detection_modules": "test1",
"content_version": "version1",
"detector_id": "ID",
"should_be_filtered1": "N",
"should_be_filtered2": "N",
"should_be_filtered3": "N",
"raw_abioc": {
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
"should_be_filtered1": "N",
"should_be_filtered2": "N",
"should_be_filtered3": "N",
}
},
}
assert filter_general_fields(alert) == {
"detection_modules": "test1",
"content_version": "version1",
"detector_id": "ID",
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
},
}
def test_filter_general_fields_with_stateful_raw_data():
"""
Given:
- An alert dict with stateful_raw_data section
When:
- Running filter_general_fields command once with events_from_decider_as_list as False and once as True.
Then:
- Verify expected output
"""
from CoreIRApiModule import filter_general_fields
alert = {
"detection_modules": "test1",
"content_version": "version1",
"detector_id": "ID",
"raw_abioc": {
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
}
},
"stateful_raw_data": {
"events_from_decider": {
"test_1": {"story_id": "test_1", "additional_info": "this is a test."},
"test_2": {"story_id": "test_2", "additional_info": "this is a test."},
}
},
}
assert filter_general_fields(alert, False, False) == {
"detection_modules": "test1",
"content_version": "version1",
"detector_id": "ID",
"raw_abioc": {
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
}
},
"stateful_raw_data": {
"events_from_decider": {
"test_1": {"story_id": "test_1", "additional_info": "this is a test."},
"test_2": {"story_id": "test_2", "additional_info": "this is a test."},
}
},
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
},
}
assert filter_general_fields(alert, False, True) == {
"detection_modules": "test1",
"content_version": "version1",
"detector_id": "ID",
"raw_abioc": {
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
}
},
"stateful_raw_data": {
"events_from_decider": [
{"story_id": "test_1", "additional_info": "this is a test."},
{"story_id": "test_2", "additional_info": "this is a test."},
]
},
"event": {
"event_type": "type",
"event_id": "id",
"identity_sub_type": "subtype",
},
}
def test_filter_general_fields_no_event(mocker):
"""
Given:
- An alert dict with no event
When:
- Running filter_general_fields command
Then:
- Verify a warning is printed and the program exits
"""
from CoreIRApiModule import filter_general_fields
alert = {
"detection_modules": "test1",
"content_version": "version1",
"detector_id": "ID",
"should_be_filtered1": "N",
"should_be_filtered2": "N",
"should_be_filtered3": "N",
"raw_abioc": {},
}
err = mocker.patch("CoreIRApiModule.return_warning")
filter_general_fields(alert)
assert err.call_args[0][0] == "No XDR cloud analytics event."
def test_add_exclusion_command(requests_mock):
"""
Given:
- FilterObject and name to add to exclision.
When:
- A user desires to add exclusion.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, add_exclusion_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
add_exclusion_response = load_test_data("./test_data/add_exclusion_response.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts_exclusion/add/", json=add_exclusion_response)
res = add_exclusion_command(
client=client,
args={
"filterObject": '{"filter":{"AND":[{"SEARCH_FIELD":"alert_category",'
'"SEARCH_TYPE":"NEQ","SEARCH_VALUE":"Phishing"}]}}',
"name": "test1",
},
)
expected_res = add_exclusion_response.get("reply")
assert res.readable_output == tableToMarkdown("Add Exclusion", expected_res)
def test_delete_exclusion_command(requests_mock):
"""
Given:
- alert_exclusion_id of the exclusion to delete.
When:
- A user desires to delete exclusion.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, delete_exclusion_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
delete_exclusion_response = load_test_data("./test_data/delete_exclusion_response.json")
alert_exclusion_id = 42
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts_exclusion/delete/",
json=delete_exclusion_response,
)
res = delete_exclusion_command(client=client, args={"alert_exclusion_id": alert_exclusion_id})
assert res.readable_output == f"Successfully deleted the following exclusion: {alert_exclusion_id}"
def test_get_exclusion_command(requests_mock):
"""
Given:
- FilterObject and name to get by exclisions.
When:
- A user desires to get exclusions.
Then:
- returns markdown, context data and raw response.
"""
from CoreIRApiModule import CoreClient, get_exclusion_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
get_exclusion_response = load_test_data("./test_data/get_exclusion_response.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts_exclusion/", json=get_exclusion_response)
res = get_exclusion_command(client=client, args={})
expected_result = get_exclusion_response.get("reply")
assert res.readable_output == tableToMarkdown("Exclusion", expected_result)
def test_get_original_alerts_command__with_filter(requests_mock):
"""
Given:
- Core client
- Alert IDs
When:
- Running get_original_alerts_command command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, get_original_alerts_command
api_response = load_test_data("./test_data/get_original_alerts_results.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts/get_original_alerts/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"alert_ids": "2", "filter_alert_fields": True}
output = get_original_alerts_command(client, args).outputs[0]
assert len(output) == 4 # make sure fields were filtered
event = output["event"]
assert len(event) == 23 # make sure fields were filtered
assert event.get("_time") == "DATE" # assert general filter is correct
assert event.get("cloud_provider") == "AWS" # assert general filter is correct
assert event.get("raw_log", {}).get("userIdentity", {}).get("accountId") == "ID" # assert vendor filter is correct
def test_get_original_alerts_command__without_filtering(requests_mock):
"""
Given:
- Core client
- Alert IDs
When:
- Running get_original_alerts_command command
Then:
- Verify expected output length
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, get_original_alerts_command
api_response = load_test_data("./test_data/get_original_alerts_results.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts/get_original_alerts/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"alert_ids": "2", "filter_alert_fields": False}
alert = get_original_alerts_command(client, args).outputs[0]
event = alert["event"]
assert len(alert) == 13 # make sure fields were not filtered
assert len(event) == 41 # make sure fields were not filtered
@pytest.mark.parametrize(
"alert_ids, raises_demisto_exception",
[
("59cf36bbdedb8f05deabf00d9ae77ee5$&$A Successful login from TOR", True),
("b0e754480d79eb14cc9308613960b84b$&$A successful SSO sign-in from TOR", True),
("9d657d2dfd14e63d0b98c9dfc3647b4f$&$A successful SSO sign-in from TOR", True),
("561675a86f68413b6e7a3b12e48c6072$&$External Login Password Spray", True),
("fe925817cddbd11e6efe5a108cf4d4c5$&$SSO Password Spray", True),
("e2d2a0dd589e8ca97d468cdb0468e94d$&$SSO Brute Force", True),
("3978e33b76cc5b2503ba60efd4445603$&$A successful SSO sign-in from TOR", True),
("79", False),
],
)
def test_get_original_alerts_command_raises_exception_playbook_debugger_input(alert_ids, raises_demisto_exception, requests_mock):
"""
Given:
- A list of alert IDs with invalid formats for the alert ID of the form <GUID>$&$<Playbook name>
When:
- Running get_original_alerts_command command
Then:
- Verify that DemistoException is raised
"""
from CoreIRApiModule import CoreClient, get_original_alerts_command
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"alert_ids": alert_ids}
if raises_demisto_exception:
with pytest.raises(DemistoException):
get_original_alerts_command(client, args)
else:
api_response = load_test_data("./test_data/get_original_alerts_results.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts/get_original_alerts/", json=api_response)
get_original_alerts_command(client, args)
def test_get_dynamic_analysis(requests_mock):
"""
Given:
- Core client
- Alert IDs
When:
- Running get_dynamic_analysis_command command
Then:
- Verify expected output
- Ensure request body sent as expected
"""
from CoreIRApiModule import CoreClient, get_dynamic_analysis_command
api_response = load_test_data("./test_data/get_dynamic_analysis.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts/get_original_alerts/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"alert_ids": "6536",
}
response = get_dynamic_analysis_command(client, args)
dynamic_analysis = response.outputs[0]
assert dynamic_analysis.get("causalityId") == "AAA"
def test_parse_get_script_execution_results():
from CoreIRApiModule import parse_get_script_execution_results
results = [
{
"endpoint_name": "endpoint_name",
"endpoint_ip_address": ["1.1.1.1"],
"endpoint_status": "endpoint_status",
"domain": "env",
"endpoint_id": "endpoint_id",
"execution_status": "COMPLETED_SUCCESSFULLY",
"standard_output": 'Running command "command_executed"',
"retrieved_files": 0,
"failed_files": 0,
"retention_date": None,
"command_executed": ["command_output"],
}
]
res = parse_get_script_execution_results(results)
expected_res = [
{
"endpoint_name": "endpoint_name",
"endpoint_ip_address": ["1.1.1.1"],
"endpoint_status": "endpoint_status",
"domain": "env",
"endpoint_id": "endpoint_id",
"execution_status": "COMPLETED_SUCCESSFULLY",
"standard_output": 'Running command "command_executed"',
"retrieved_files": 0,
"failed_files": 0,
"retention_date": None,
"command_executed": ["command_output"],
"command": "command_executed",
"command_output": ["command_output"],
}
]
assert res == expected_res
class TestGetAlertByFilter:
@freeze_time("2022-05-03 11:00:00 GMT")
def test_get_alert_by_filter(self, requests_mock, mocker):
"""
Given:
- Core client
- timeframe, start_time, end_time
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected
"""
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/",
json=api_response,
)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"time_frame": "custom",
"start_time": "2018-11-06T08:56:41",
"end_time": "2018-11-06T08:56:41",
"limit": "2",
}
response = get_alerts_by_filter_command(client, args)
assert response.outputs[0].get("internal_id", {}) == 33333
assert (
"{'filter_data': {'sort': [{'FIELD': 'source_insert_ts', 'ORDER': 'DESC'}], 'paging': {'from': 0, "
"'to': 2}, 'filter': {'AND': [{'SEARCH_FIELD': 'source_insert_ts', 'SEARCH_TYPE': 'RANGE', "
"'SEARCH_VALUE': {'from': 1541494601000, 'to': 1541494601000}}]}}}" in request_data_log.call_args[0][0]
)
def test_get_alert_by_alert_action_status_filter(self, requests_mock, mocker):
"""
Given:
- Core client
- Alert with action status of SCANNED
When:
- Running get_alerts_by_filter command with alert_action_status="detected (scanned)"
Then:
- Verify the alert in the output contains alert_action_status and alert_action_status_readable
- Ensure request filter contains the alert_action_status as SCANNED
"""
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/",
json=api_response,
)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"alert_action_status": "detected (scanned)"}
response = get_alerts_by_filter_command(client, args)
assert response.outputs[0].get("internal_id", {}) == 33333
assert response.outputs[0].get("alert_action_status", {}) == "SCANNED"
assert response.outputs[0].get("alert_action_status_readable", {}) == "detected (scanned)"
assert (
"{'SEARCH_FIELD': 'alert_action_status', 'SEARCH_TYPE': 'EQ', 'SEARCH_VALUE': "
"'SCANNED'" in request_data_log.call_args[0][0]
)
def test_get_alert_by_filter_command_multiple_values_in_same_arg(self, requests_mock, mocker):
"""
Given:
- Core client
- alert_source
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected (connected with OR operator)
"""
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/",
json=api_response,
)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"alert_source": "first,second",
}
response = get_alerts_by_filter_command(client, args)
assert response.outputs[0].get("internal_id", {}) == 33333
assert (
"{'filter_data': {'sort': [{'FIELD': 'source_insert_ts', 'ORDER': 'DESC'}], 'paging': {'from': 0, "
"'to': 50}, 'filter': {'AND': [{'OR': [{'SEARCH_FIELD': 'alert_source', 'SEARCH_TYPE': 'CONTAINS', "
"'SEARCH_VALUE': 'first'}, {'SEARCH_FIELD': 'alert_source', 'SEARCH_TYPE': 'CONTAINS', "
"'SEARCH_VALUE': 'second'}]}]}}}" in request_data_log.call_args[0][0]
)
def test_get_alert_by_filter_command_multiple_args(self, requests_mock, mocker):
"""
Given:
- Core client
- alert_source
- user_name
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected (connected with AND operator)
"""
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/",
json=api_response,
)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"alert_source": "first,second", "user_name": "N/A"}
response = get_alerts_by_filter_command(client, args)
assert response.outputs[0].get("internal_id", {}) == 33333
assert (
"{'AND': [{'OR': [{'SEARCH_FIELD': 'alert_source', 'SEARCH_TYPE': 'CONTAINS', "
"'SEARCH_VALUE': 'first'}, {'SEARCH_FIELD': 'alert_source', 'SEARCH_TYPE': 'CONTAINS', "
"'SEARCH_VALUE': 'second'}]}, {'OR': [{'SEARCH_FIELD': 'actor_effective_username', "
"'SEARCH_TYPE': 'CONTAINS', 'SEARCH_VALUE': 'N/A'}]}]}" in request_data_log.call_args[0][0]
)
@freeze_time("2022-05-26T13:00:00Z")
def test_get_alert_by_filter_complex_custom_filter_and_timeframe(self, requests_mock, mocker):
"""
Given:
- Core client
- custom_filter (filters are connected with AND operator)
- timeframe
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected (connected with AND operator)
"""
from datetime import datetime as dt
import dateparser
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
custom_filter = (
'{"AND": [{"OR": [{"SEARCH_FIELD": "alert_source","SEARCH_TYPE": "EQ",'
'"SEARCH_VALUE": "CORRELATION"},'
'{"SEARCH_FIELD": "alert_source","SEARCH_TYPE": "EQ","SEARCH_VALUE": "IOC"}]},'
'{"SEARCH_FIELD": "severity","SEARCH_TYPE": "EQ","SEARCH_VALUE": "SEV_040_HIGH"}]}'
)
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/",
json=api_response,
)
request_data_log = mocker.patch.object(demisto, "debug")
mocker.patch.object(
dateparser,
"parse",
return_value=dt(year=2022, month=5, day=24, hour=13, minute=0, second=0),
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"custom_filter": custom_filter, "time_frame": "2 days"}
get_alerts_by_filter_command(client, args)
assert (
"{'filter_data': {'sort': [{'FIELD': 'source_insert_ts', 'ORDER': 'DESC'}], "
"'paging': {'from': 0, 'to': 50}, "
"'filter': {'AND': [{'SEARCH_FIELD': 'source_insert_ts', 'SEARCH_TYPE': 'RELATIVE_TIMESTAMP', "
"'SEARCH_VALUE': '172800000'}, "
"{'OR': [{'SEARCH_FIELD': 'alert_source', 'SEARCH_TYPE': 'EQ', 'SEARCH_VALUE': 'CORRELATION'}, "
"{'SEARCH_FIELD': 'alert_source', 'SEARCH_TYPE': 'EQ', 'SEARCH_VALUE': 'IOC'}]}, "
"{'SEARCH_FIELD': 'severity', 'SEARCH_TYPE': 'EQ', 'SEARCH_VALUE': 'SEV_040_HIGH'}]}}}"
in request_data_log.call_args[0][0]
)
@freeze_time("2022-05-26T13:00:00Z")
def test_get_alert_by_filter_custom_filter_and_timeframe_(self, requests_mock, mocker):
"""
Given:
- Core client
- custom_filter (filters are connected with OR operator)
- timeframe
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected (connected with AND operator)
"""
from datetime import datetime as dt
import dateparser
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
custom_filter = '{"OR": [{"SEARCH_FIELD": "actor_process_image_sha256","SEARCH_TYPE": "EQ","SEARCH_VALUE": "222"}]}'
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/",
json=api_response,
)
request_data_log = mocker.patch.object(demisto, "debug")
mocker.patch.object(
dateparser,
"parse",
return_value=dt(year=2022, month=5, day=24, hour=13, minute=0, second=0),
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"custom_filter": custom_filter, "time_frame": "2 days"}
get_alerts_by_filter_command(client, args)
assert (
"{'filter_data': {'sort': [{'FIELD': 'source_insert_ts', 'ORDER': 'DESC'}], "
"'paging': {'from': 0, 'to': 50}, "
"'filter': {'AND': [{'SEARCH_FIELD': 'source_insert_ts', 'SEARCH_TYPE': 'RELATIVE_TIMESTAMP', "
"'SEARCH_VALUE': '172800000'}, "
"{'OR': [{'SEARCH_FIELD': 'actor_process_image_sha256', 'SEARCH_TYPE': 'EQ',"
" 'SEARCH_VALUE': '222'}]}]}" in request_data_log.call_args[0][0]
)
class TestGetIssueByFilter:
@freeze_time("2022-05-03 11:00:00 GMT")
def test_get_alert_by_filter(self, requests_mock, mocker):
"""
Given:
- Core client
- timeframe, start_time, end_time
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = load_test_data("./test_data/get_issues_by_filter_results.json")
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
args = {
"start_time": "2018-11-06T08:56:41",
"end_time": "2018-11-06T08:56:41",
"limit": "2",
}
response = get_issues_by_filter_command(client, args)
assert response[0].outputs[0].get("internal_id", {}) == 33333
def test_get_issues_by_alert_action_status_filter(self, requests_mock, mocker):
"""
Given:
- Core client
- Alert with action status of SCANNED
When:
- Running get_alerts_by_filter command with alert_action_status="detected (scanned)"
Then:
- Verify the alert in the output contains alert_action_status and alert_action_status_readable
- Ensure request filter contains the alert_action_status as SCANNED
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = load_test_data("./test_data/get_issues_by_filter_results.json")
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
args = {"issue_action_status": "detected (scanned)"}
response = get_issues_by_filter_command(client, args)
assert response[0].outputs[0].get("internal_id", {}) == 33333
assert response[0].outputs[0].get("alert_action_status", {}) == "SCANNED"
assert response[0].outputs[0].get("alert_action_status_readable", {}) == "detected (scanned)"
# Verify the request data was logged and contains the correct filter
assert request_data_log.call_count > 0
logged_request = request_data_log.call_args_list[0][0][0]
assert "SEARCH_FIELD': 'alert_action_status'" in logged_request
assert "SEARCH_TYPE': 'EQ'" in logged_request
assert "SEARCH_VALUE': 'SCANNED'" in logged_request
def test_get_issues_by_filter_command_multiple_values_in_same_arg(self, requests_mock, mocker):
"""
Given:
- Core client
- alert_source
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected (connected with OR operator)
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = load_test_data("./test_data/get_issues_by_filter_results.json")
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
args = {"issue_source": "first,second"}
response = get_issues_by_filter_command(client, args)
assert response[0].outputs[0].get("internal_id", {}) == 33333
# Verify the request data was logged and contains the correct filter
assert request_data_log.call_count > 0
logged_request = request_data_log.call_args_list[0][0][0]
assert "'SEARCH_FIELD': 'alert_source'" in logged_request
assert "'SEARCH_TYPE': 'CONTAINS'" in logged_request
assert "'SEARCH_VALUE': 'first'" in logged_request
assert "'SEARCH_VALUE': 'second'" in logged_request
def test_get_issues_by_filter_command_multiple_args(self, requests_mock, mocker):
"""
Given:
- Core client
- alert_source
- user_name
When:
- Running get_alerts_by_filter command
Then:
- Verify expected output
- Ensure request filter sent as expected (connected with AND operator)
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = load_test_data("./test_data/get_issues_by_filter_results.json")
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
request_data_log = mocker.patch.object(demisto, "debug")
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
args = {"issue_source": "first,second", "user_name": "N/A"}
response = get_issues_by_filter_command(client, args)
assert response[0].outputs[0].get("internal_id", {}) == 33333
# Verify the request data was logged and contains the correct filter
assert request_data_log.call_count > 0
logged_request = request_data_log.call_args_list[0][0][0]
# Check for source filter
assert "'SEARCH_FIELD': 'alert_source'" in logged_request
assert "'SEARCH_TYPE': 'CONTAINS'" in logged_request
assert "'SEARCH_VALUE': 'first'" in logged_request
assert "'SEARCH_VALUE': 'second'" in logged_request
# Check for user name filter
assert "'SEARCH_FIELD': 'actor_effective_username'" in logged_request
assert "'SEARCH_TYPE': 'CONTAINS'" in logged_request
assert "'SEARCH_VALUE': 'N/A'" in logged_request
class TestPollingCommands:
@staticmethod
def create_mocked_responses(status_count):
response_queue = [ # xdr-run-script response
{"reply": {"action_id": 1, "status": 1, "endpoints_count": 1}}
]
for i in range(status_count):
if i == status_count - 1:
general_status = "COMPLETED_SUCCESSFULLY"
elif i < 2:
general_status = "PENDING"
else:
general_status = "IN_PROGRESS"
response_queue.append(
{
"reply": { # get script status response
"general_status": general_status,
"endpoints_pending": 1 if i < 2 else 0,
"endpoints_in_progress": 0 if i < 2 else 1,
}
}
)
response_queue.append(
{
"reply": { # get script execution result response
"script_name": "snippet script",
"error_message": "",
"results": [
{
"endpoint_name": "test endpoint",
"endpoint_ip_address": ["1.1.1.1"],
"endpoint_status": "STATUS_010_CONNECTED",
"domain": "aaaa",
"endpoint_id": "1",
"execution_status": "COMPLETED_SUCCESSFULLY",
"failed_files": 0,
}
],
}
}
)
return response_queue
@pytest.mark.parametrize(argnames="status_count", argvalues=[1, 3, 7, 9, 12, 15])
def test_script_run_command(self, mocker, status_count):
"""
Given -
xdr-script-run command arguments including polling true where each time a different amount of response
is returned.
When: -
Running the xdr-script-run
Then:
- Make sure the readable output is returned to war-room only once indicating on polling.
- Make sure the correct context output is returned once the command finished polling
- Make sure context output is returned only at the end of polling.
- Make sure the readable output is returned only in the first run.
- Make sure the correct output prefix is returned.
"""
from CommonServerPython import ScheduledCommand
from CoreIRApiModule import script_run_polling_command
client = CoreClient(base_url="https://test_api.com/public_api/v1", headers={})
mocker.patch.object(
client,
"_http_request",
side_effect=self.create_mocked_responses(status_count),
)
mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)
command_result = script_run_polling_command({"endpoint_ids": "1", "script_uid": "1"}, client)
assert command_result.readable_output == "Waiting for the script to finish running on the following endpoints: ['1']..."
assert command_result.outputs == {
"action_id": 1,
"endpoints_count": 1,
"status": 1,
}
polling_args = {
"endpoint_ids": "1",
"script_uid": "1",
"action_id": "1",
"hide_polling_output": True,
}
command_result = script_run_polling_command(polling_args, client)
# if scheduled_command is set, it means that command should still poll
while not isinstance(command_result, list) and command_result.scheduled_command:
# if command result is a list, it means command execution finished
assert not command_result.readable_output # make sure that indication of polling is printed only once
# make sure no context output is being returned to war-room during polling
assert not command_result.outputs
command_result = script_run_polling_command(polling_args, client)
assert command_result[0].outputs == {
"action_id": 1,
"results": [
{
"endpoint_name": "test endpoint",
"endpoint_ip_address": ["1.1.1.1"],
"endpoint_status": "STATUS_010_CONNECTED",
"domain": "aaaa",
"endpoint_id": "1",
"execution_status": "COMPLETED_SUCCESSFULLY",
"failed_files": 0,
}
],
}
assert command_result[0].outputs_prefix == "PaloAltoNetworksXDR.ScriptResult"
@pytest.mark.parametrize(
"args, expected_filters, func, url_suffix, expected_human_readable",
[
(
{"endpoint_ids": "1,2", "tag": "test"},
[{"field": "endpoint_id_list", "operator": "in", "value": ["1", "2"]}],
add_tag_to_endpoints_command,
"/tags/agents/assign/",
"Successfully added tag test to endpoint(s) ['1', '2']",
),
(
{"endpoint_ids": "1,2", "tag": "test", "status": "disconnected"},
[{"field": "endpoint_status", "operator": "IN", "value": ["disconnected"]}],
add_tag_to_endpoints_command,
"/tags/agents/assign/",
"Successfully added tag test to endpoint(s) ['1', '2']",
),
(
{
"endpoint_ids": "1,2",
"tag": "test",
"hostname": "hostname",
"group_name": "test_group",
},
[
{"field": "group_name", "operator": "in", "value": ["test_group"]},
{"field": "hostname", "operator": "in", "value": ["hostname"]},
],
add_tag_to_endpoints_command,
"/tags/agents/assign/",
"Successfully added tag test to endpoint(s) ['1', '2']",
),
(
{"endpoint_ids": "1,2", "tag": "test"},
[{"field": "endpoint_id_list", "operator": "in", "value": ["1", "2"]}],
remove_tag_from_endpoints_command,
"/tags/agents/remove/",
"Successfully removed tag test from endpoint(s) ['1', '2']",
),
(
{"endpoint_ids": "1,2", "tag": "test", "platform": "linux"},
[{"field": "platform", "operator": "in", "value": ["linux"]}],
remove_tag_from_endpoints_command,
"/tags/agents/remove/",
"Successfully removed tag test from endpoint(s) ['1', '2']",
),
(
{
"endpoint_ids": "1,2",
"tag": "test",
"isolate": "isolated",
"alias_name": "alias_name",
},
[
{"field": "alias", "operator": "in", "value": ["alias_name"]},
{"field": "isolate", "operator": "in", "value": ["isolated"]},
],
remove_tag_from_endpoints_command,
"/tags/agents/remove/",
"Successfully removed tag test from endpoint(s) ['1', '2']",
),
],
)
def test_add_or_remove_tag_endpoint_command(requests_mock, args, expected_filters, func, url_suffix, expected_human_readable):
"""
Given:
- command arguments
- expected filters as a body request
When:
- executing the core-add-tag-endpoint command
Then:
- make sure the body request was sent as expected to the api request and that human readable is valid.
"""
client = CoreClient(base_url=f"{Core_URL}/public_api/v1/", headers={})
add_tag_mock = requests_mock.post(f"{Core_URL}/public_api/v1{url_suffix}", json={})
result = func(client=client, args=args)
assert result.readable_output == expected_human_readable
assert add_tag_mock.last_request.json() == {
"context": {
"lcaas_id": ["1", "2"],
},
"request_data": {"filters": expected_filters, "tag": "test"},
}
excepted_output_1 = {
"filters": [{"field": "endpoint_status", "operator": "IN", "value": ["connected"]}],
"new_alias_name": "test",
}
excepted_output_2 = {
"filters": [{"field": "endpoint_status", "operator": "IN", "value": ["connected"]}],
"new_alias_name": "",
}
@pytest.mark.parametrize("input, expected_output", [("test", excepted_output_1), ('""', excepted_output_2)])
def test_endpoint_alias_change_command__diffrent_alias_new_names(mocker, input, expected_output):
"""
Given:
- valid new alias name as string - empty new alias name (due to xsoar limitation,
represented by a string of double quote)
When:
- executing the endpoint-alias-change command
Then:
- Makes sure the request body is created correctly.
"""
client = CoreClient(base_url=f"{Core_URL}/public_api/v1/", headers={})
mocker_set = mocker.patch.object(client, "set_endpoints_alias")
from CoreIRApiModule import endpoint_alias_change_command
endpoint_alias_change_command(client=client, status="connected", new_alias_name=input)
assert mocker_set.call_args[1] == expected_output
def test_endpoint_alias_change_command__no_filters(mocker):
"""
Given:
- command withot endpoint filters
When:
- executing the endpoint-alias-change command
Then:
- make sure the correct error message wil raise.
"""
client = CoreClient(base_url=f"{Core_URL}/public_api/v1/", headers={})
mocker.patch.object(client, "set_endpoints_alias")
from CoreIRApiModule import endpoint_alias_change_command
with pytest.raises(Exception) as e:
endpoint_alias_change_command(client=client, new_alias_name="test")
assert e.value.message == "Please provide at least one filter."
GRACEFULLY_FAILING = [
pytest.param(
quarantine_files_command,
{
"endpoint_id_list": "123",
"file_path": "C:\\Users\\test\\Desktop\\test_x64.msi",
"file_hash": "123",
},
{
"err_msg": "An error occurred while processing XDR public API - No endpoint "
"was found "
"for creating the requested action",
"status_code": 500,
},
False,
id="Success",
),
pytest.param(
isolate_endpoint_command,
{"endpoint_id": "1111"},
{"err_msg": "Other error", "status_code": 401},
True,
id="Failure",
),
]
@pytest.mark.parametrize("command_to_run, args, error, raises", GRACEFULLY_FAILING)
def test_core_commands_raise_exception(mocker, command_to_run, args, error, raises):
"""
Given:
- XDR API error.
When:
- executing the isolate-endpoint-command and quarantine-files-command command
Then:
- make sure the correct error message wil raise.
"""
class MockException:
def __init__(self, status_code) -> None:
self.status_code = status_code
client = CoreClient(base_url=f"{Core_URL}/public_api/v1/", headers={})
mocker.patch.object(
client,
"_http_request",
side_effect=DemistoException(error.get("err_msg"), res=MockException(error.get("status_code"))),
)
if raises:
with pytest.raises(Exception) as e:
command_to_run(client, args)
assert "Other error" in str(e)
else:
assert command_to_run(client, args).readable_output == "The operation executed is not supported on the given machine."
@pytest.mark.parametrize(
"command, func_http, args, excepted_calls, path_test_data",
[
(
"user",
"list_risky_users",
{"user_id": "test"},
{"risk_score_user_or_host": 1, "list_risky_users": 0},
"./test_data/list_risky_users_hosts.json",
),
(
"user",
"list_risky_users",
{},
{"risk_score_user_or_host": 0, "list_risky_users": 1},
"./test_data/list_risky_users.json",
),
(
"host",
"list_risky_hosts",
{"host_id": "test"},
{"risk_score_user_or_host": 1, "list_risky_hosts": 0},
"./test_data/list_risky_users_hosts.json",
),
(
"host",
"list_risky_hosts",
{},
{"risk_score_user_or_host": 0, "list_risky_hosts": 1},
"./test_data/list_risky_hosts.json",
),
],
)
def test_list_risky_users_or_hosts_command(
mocker,
command: str,
func_http: str,
args: dict[str, str],
excepted_calls: dict[str, int],
path_test_data: str,
):
"""
Test case to verify the behavior of the 'list_risky_users_or_hosts_command' function.
Args:
mocker (Any): The mocker object to patch the required methods.
command (str): The command to be tested ('user' or 'host').
func_http (str): The name of the HTTP function to be called.
args (dict[str, str]): The arguments for the command.
expected_calls (dict[str, int]): The expected number of calls for each mocked method.
Returns:
None
"""
test_data = load_test_data(path_test_data)
client = CoreClient("test", {})
risk_by_user_or_host = mocker.patch.object(CoreClient, "risk_score_user_or_host", return_value=test_data)
list_risky_users = mocker.patch.object(CoreClient, func_http, return_value=test_data)
result = list_risky_users_or_host_command(client=client, command=command, args=args)
assert result.outputs == test_data["reply"]
assert risk_by_user_or_host.call_count == excepted_calls["risk_score_user_or_host"]
assert list_risky_users.call_count == excepted_calls[func_http]
@pytest.mark.parametrize(
"command ,id_",
[
("user", "user_id"),
("host", "host_id"),
],
)
def test_list_risky_users_hosts_command_raise_exception(mocker, command: str, id_: str):
"""
Given:
- XDR API error indicating that the user / host was not found
When:
- executing the list_risky_users_or_host_command function
Then:
- make sure a message indicating that the user was not found is returned
"""
client = CoreClient(
base_url="test",
headers={},
)
class MockException:
def __init__(self, status_code) -> None:
self.status_code = status_code
mocker.patch.object(
client,
"risk_score_user_or_host",
side_effect=DemistoException(message="id 'test' was not found", res=MockException(500)),
)
result = list_risky_users_or_host_command(client, command, {id_: "test"})
assert result.readable_output == f"The {command} test was not found"
@pytest.mark.parametrize(
"command ,args, client_func",
[
("user", {"user_id": "test"}, "risk_score_user_or_host"),
("host", {"host_id": "test"}, "risk_score_user_or_host"),
("user", {}, "list_risky_users"),
("host", {}, "list_risky_hosts"),
],
ids=["user_id", "host_id", "list_users", "list_hosts"],
)
def test_list_risky_users_hosts_command_no_license_warning(mocker: MockerFixture, command: str, args: dict, client_func: str):
"""
Given:
- XDR API error indicating that the user / host was not found
When:
- executing the list_risky_users_or_host_command function
Then:
- make sure a message indicating that the user was not found is returned
"""
client = CoreClient(
base_url="test",
headers={},
)
class MockException:
def __init__(self, status_code) -> None:
self.status_code = status_code
mocker.patch.object(
client,
client_func,
side_effect=DemistoException(
message="An error occurred while processing XDR public API, No identity threat",
res=MockException(500),
),
)
import CoreIRApiModule
warning = mocker.patch.object(CoreIRApiModule, "return_warning")
with pytest.raises(DemistoException):
list_risky_users_or_host_command(client, command, args)
assert warning.call_args[0][0] == (
"Please confirm the XDR Identity Threat Module is enabled.\n"
"Full error message: An error occurred while processing XDR public API,"
" No identity threat"
)
assert warning.call_args[1] == {"exit": True}
def test_list_user_groups_command(mocker):
"""
Test function to validate the behavior of the `list_user_groups_command` function.
Args:
mocker: Pytest mocker object.
args (dict): A dictionary containing optional `group_names` argument.
Returns:
None.
Raises:
AssertionError: If the expected output doesn't match the actual output.
"""
client = CoreClient("test", {})
test_data = load_test_data("./test_data/get_list_user_groups.json")
mocker.patch.object(CoreClient, "list_user_groups", return_value=test_data)
results = list_user_groups_command(client=client, args={"group_names": "test"})
assert test_data["reply"] == results.outputs
@pytest.mark.parametrize(
"data, expected_results",
[
(
{
"group_name": "Group2",
"description": None,
"pretty_name": "dummy1",
"insert_time": 1111111111111,
"update_time": 2222222222222,
"user_email": [
"dummy1@gmail.com",
"dummy2@gmail.com",
],
"source": "Custom",
},
[
{
"User email": "dummy1@gmail.com",
"Group Name": "Group2",
"Group Description": None,
},
{
"User email": "dummy2@gmail.com",
"Group Name": "Group2",
"Group Description": None,
},
],
)
],
)
def test_parse_user_groups(data: dict[str, Any], expected_results: list[dict[str, Any]]):
"""
Test the 'parse_user_groups' function that parses user group information.
Args:
data (dict): A dictionary containing a sample user group data.
Returns:
None.
Raises:
AssertionError: If the parsing of user groups data fails.
"""
assert parse_user_groups(data) == expected_results
@pytest.mark.parametrize(
"test_data, excepted_error",
[
(
{"group_names": "test"},
"Error: Group test was not found. Full error message: Group 'test' was not found",
),
(
{"group_names": "test, test2"},
"Error: Group test was not found. Note: If you sent more than one group name, "
"they may not exist either. Full error message: Group 'test' was not found",
),
],
)
def test_list_user_groups_command_raise_exception(mocker, test_data: dict[str, str], excepted_error: str):
"""
Tests that the 'list_user_groups_command' function raises an exception When: the 'list_user_groups' method of
the 'CoreClient' class raises a 'DemistoException'.
Args:
mocker: The pytest mocker object.
Raises:
Exception: If the 'list_user_groups_command' function does not raise an exception When: expected.
Returns:
None.
"""
client = CoreClient(
base_url="test",
headers={},
)
class MockException:
def __init__(self, status_code) -> None:
self.status_code = status_code
mocker.patch.object(
client,
"list_user_groups",
side_effect=DemistoException(message="Group 'test' was not found", res=MockException(500)),
)
with pytest.raises(
DemistoException,
match=excepted_error,
):
list_user_groups_command(client, test_data)
def test_list_users_command(mocker):
"""
Tests the `list_users_command` function.
Args:
mocker: The pytest mocker object.
Returns:
None.
Raises:
AssertionError: If the test fails.
"""
client = CoreClient("test", {})
test_data = load_test_data("./test_data/get_list_users.json")
mocker.patch.object(CoreClient, "list_users", return_value=test_data)
results = list_users_command(client=client, args={})
assert test_data["reply"] == results.outputs
@pytest.mark.parametrize(
"role_data",
[
{
"reply": [
[
{
"pretty_name": "test",
"description": "test",
"permissions": "test",
"users": "test",
"groups": "test",
}
]
]
},
],
)
def test_list_roles_command(mocker, role_data: dict[str, str]) -> None:
"""
Tests the 'list_roles_command' function.
Args:
mocker: A pytest-mock object.
role_data (dict): A dictionary containing the test data for the roles.
expected_output (str): The expected output for the test.
Raises:
AssertionError: If the test fails.
"""
client = CoreClient("test", {})
mocker.patch.object(CoreClient, "list_roles", return_value=role_data)
results = list_roles_command(client=client, args={"role_names": "test"})
assert role_data["reply"] == results.outputs
@pytest.mark.parametrize(
"func, args, update_count, expected_output",
[
(
"remove_user_role",
{"user_emails": "test1@example.com,test2@example.com"},
{"reply": {"update_count": "2"}},
"Role was removed successfully for 2 users.",
),
(
"remove_user_role",
{"user_emails": "test1@example.com,test2@example.com"},
{"reply": {"update_count": "1"}},
"Role was removed successfully for 1 user.",
),
(
"set_user_role",
{
"user_emails": "test1@example.com,test2@example.com",
"role_name": "admin",
},
{"reply": {"update_count": "2"}},
"Role was updated successfully for 2 users.",
),
],
)
def test_change_user_role_command_happy_path(
mocker,
func: str,
args: dict[str, str],
update_count: dict[str, dict[str, str]],
expected_output: str,
):
"""
Given:
- Valid user emails and role name provided.
When:
- Running the change_user_role_command function.
Then:
- Ensure the function returns a CommandResults object with the expected readable output.
"""
client = CoreClient("test", {})
mocker.patch.object(CoreClient, func, return_value=update_count)
result = change_user_role_command(client, args)
assert result.readable_output == expected_output
@pytest.mark.parametrize(
"func, args, update_count, expected_output",
[
(
"remove_user_role",
{"user_emails": "test1@example.com,test2@example.com"},
{"reply": {"update_count": 0}},
"No user role has been removed.",
),
(
"set_user_role",
{
"user_emails": "test1@example.com,test2@example.com",
"role_name": "admin",
},
{"reply": {"update_count": 0}},
"No user role has been updated.",
),
],
)
def test_change_user_role_command_with_raise(
mocker,
func: str,
args: dict[str, str],
update_count: dict[str, dict[str, int]],
expected_output: str,
):
client = CoreClient("test", {})
mocker.patch.object(CoreClient, func, return_value=update_count)
with pytest.raises(DemistoException, match=expected_output):
change_user_role_command(client, args)
def test_endpoint_command_fails(requests_mock):
"""
Given:
- no arguments
When:
- we mock the endpoint command
Then:
- Validate that there is a correct error
"""
from CoreIRApiModule import CoreClient, endpoint_command
get_endpoints_response = load_test_data("./test_data/get_endpoints.json")
requests_mock.post(f"{Core_URL}/public_api/v1/endpoints/get_endpoint/", json=get_endpoints_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args: dict = {}
with pytest.raises(DemistoException) as e:
endpoint_command(client, args)
assert "In order to run this command, please provide a valid id, ip or hostname" in str(e)
def test_generate_files_dict(mocker):
"""
Given:
- no arguments
When:
- we mock the get_endpoints command with mac, linux and windows endpoints
Then:
- Validate that the dict is generated right
"""
mocker.patch.object(
test_client,
"get_endpoints",
side_effect=[
load_test_data("test_data/get_endpoints_mac_response.json"),
load_test_data("test_data/get_endpoints_linux_response.json"),
load_test_data("test_data/get_endpoints_windows_response.json"),
],
)
res = test_client.generate_files_dict(
endpoint_id_list=["1", "2", "3"],
file_path_list=["fake\\path1", "fake\\path2", "fake\\path3"],
)
assert res == {
"macos": ["fake\\path1"],
"linux": ["fake\\path2"],
"windows": ["fake\\path3"],
}
def test_get_script_execution_result_files(mocker):
"""
Given:
- no arguments
When:
- executing the get_script_execution_result_files command
Then:
- Validate that the url_suffix generated correctly
"""
http_request = mocker.patch.object(
test_client,
"_http_request",
return_value={"reply": {"DATA": "https://test_api/public_api/v1/download/test"}},
)
test_client.get_script_execution_result_files(action_id="1", endpoint_id="1")
http_request.assert_called_with(method="GET", url_suffix="download/test", resp_type="response")
@pytest.mark.parametrize(
"error_message, expected_error_message",
[
(
"id 'test' was not found",
["Error: id test was not found. Full error message: id 'test' was not found"],
),
("some error", [None]),
],
)
def test_enrich_error_message_id_group_role(error_message: str, expected_error_message: list):
"""
Test case for the enrich_error_message_id_group_role function.
Args:
error_message (str): The error message to be passed to the function.
expected_error_message (str): The expected error message after enriching.
Raises:
AssertionError: If the error response from the function does not match the expected error message.
"""
class MockException:
def __init__(self, status_code) -> None:
self.status_code = status_code
error_response = enrich_error_message_id_group_role(
DemistoException(message=error_message, res=MockException(500)), "test", "test"
)
assert error_response == expected_error_message[0]
def get_incident_by_status(
incident_id_list=None,
lte_modification_time=None,
gte_modification_time=None,
lte_creation_time=None,
gte_creation_time=None,
starred=None,
starred_incidents_fetch_window=None,
status=None,
sort_by_modification_time=None,
sort_by_creation_time=None,
page_number=0,
limit=100,
gte_creation_time_milliseconds=0,
):
"""
The function simulate the client.get_incidents method for the test_fetch_incidents_filtered_by_status
and for the test_get_incident_list_by_status.
The function got the status as a string, and return from the json file only the incidents
that are in the given status.
"""
incidents_list = load_test_data("./test_data/get_incidents_list.json")["reply"]["incidents"]
return [incident for incident in incidents_list if incident["status"] == status]
class TestGetIncidents:
def test_get_incident_list(self, requests_mock):
"""
Given: Incidents returned from client.
When: Running get_incidents_command.
Then: Ensure the outputs contain the incidents from the client.
"""
get_incidents_list_response = load_test_data("./test_data/get_incidents_list.json")
requests_mock.post(
f"{Core_URL}/public_api/v1/incidents/get_incidents/",
json=get_incidents_list_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"incident_id_list": "1 day"}
_, outputs, _ = get_incidents_command(client, args)
expected_output = {
"CoreApiModule.Incident(val.incident_id==obj.incident_id)": get_incidents_list_response.get("reply").get("incidents")
}
assert expected_output == outputs
def test_get_incident_list_by_status(self, mocker):
"""
Given: A status query, and incidents filtered by the query.
When: Running get_incidents_command.
Then: Ensure outputs contain the incidents from the client.
"""
get_incidents_list_response = load_test_data("./test_data/get_incidents_list.json")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {"incident_id_list": "1 day", "status": "under_investigation,new"}
mocker.patch.object(client, "get_incidents", side_effect=get_incident_by_status)
_, outputs, _ = get_incidents_command(client, args)
expected_output = {
"CoreApiModule.Incident(val.incident_id==obj.incident_id)": get_incidents_list_response.get("reply").get("incidents")
}
assert expected_output == outputs
@freeze_time("2024-01-15 17:00:00 UTC")
@pytest.mark.parametrize(
"starred, expected_starred",
[
(True, True),
(False, False),
("true", True),
("false", False),
(None, None),
("", None),
],
)
def test_get_starred_incident_list_from_get(self, mocker, requests_mock, starred, expected_starred):
"""
Given: A query with starred parameters.
When: Running get_incidents_command.
Then: Ensure the starred output is returned and the request filters are set correctly.
"""
get_incidents_list_response = load_test_data("./test_data/get_starred_incidents_list.json")
get_incidents_request = requests_mock.post(
f"{Core_URL}/public_api/v1/incidents/get_incidents/",
json=get_incidents_list_response,
)
mocker.patch.object(demisto, "command", return_value="get-incidents")
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"incident_id_list": "1 day",
"starred": starred,
"starred_incidents_fetch_window": "3 days",
}
starred_filter_true = {"field": "starred", "operator": "eq", "value": True}
starred_filter_false = {"field": "starred", "operator": "eq", "value": False}
starred_fetch_window_filter = {
"field": "creation_time",
"operator": "gte",
"value": 1705078800000,
}
_, outputs, _ = get_incidents_command(client, args)
request_filters = get_incidents_request.last_request.json()["request_data"]["filters"]
assert len(outputs["CoreApiModule.Incident(val.incident_id==obj.incident_id)"]) >= 1
if expected_starred:
assert starred_filter_true in request_filters
assert starred_fetch_window_filter in request_filters
assert outputs["CoreApiModule.Incident(val.incident_id==obj.incident_id)"][0]["starred"] is True
elif expected_starred is False:
assert starred_filter_false in request_filters
assert starred_fetch_window_filter not in request_filters
else: # expected_starred is None
assert starred_filter_true not in request_filters
assert starred_filter_false not in request_filters
assert starred_fetch_window_filter not in request_filters
@freeze_time("2024-01-15 17:00:00 UTC")
@pytest.mark.parametrize("starred", [False, "False", "false", None, ""])
def test_get_starred_false_incident_list_from_fetch(self, mocker, requests_mock, starred):
"""
Given: A query with starred=false parameter.
When: Running get_incidents_command from fetch-incidents.
Then: Ensure the request doesn't filter on starred incidents.
"""
get_incidents_list_response = load_test_data("./test_data/get_starred_incidents_list.json")
mocker.patch.object(demisto, "command", return_value="fetch-incidents")
get_incidents_request = requests_mock.post(
f"{Core_URL}/public_api/v1/incidents/get_incidents/",
json=get_incidents_list_response,
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"incident_id_list": "1 day",
"starred": starred,
"starred_incidents_fetch_window": "3 days",
}
starred_filter_true = {"field": "starred", "operator": "eq", "value": True}
starred_filter_false = {"field": "starred", "operator": "eq", "value": False}
starred_fetch_window_filter = {
"field": "creation_time",
"operator": "gte",
"value": 1705078800000,
}
_, outputs, _ = get_incidents_command(client, args)
request_filters = get_incidents_request.last_request.json()["request_data"]["filters"]
assert len(outputs["CoreApiModule.Incident(val.incident_id==obj.incident_id)"]) >= 1
assert starred_filter_true not in request_filters
assert starred_filter_false not in request_filters
assert starred_fetch_window_filter not in request_filters
@freeze_time("2024-01-15 17:00:00 UTC")
@pytest.mark.parametrize("starred", [True, "true", "True"])
def test_get_starred_true_incident_list_from_fetch(self, mocker, starred):
"""
Given: A query with starred=true parameter.
When: Running get_incidents_command from fetch-incidents.
Then: Ensure the request filters on starred incidents and contains the starred_fetch_window_filter filter.
"""
get_incidents_list_response = load_test_data("./test_data/get_starred_incidents_list.json")
mocker.patch.object(demisto, "command", return_value="fetch-incidents")
handle_fetch_starred_mock = mocker.patch.object(
CoreClient,
"handle_fetch_starred_incidents",
return_value=get_incidents_list_response["reply"]["incidents"],
)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
args = {
"incident_id_list": "1 day",
"starred": starred,
"starred_incidents_fetch_window": "3 days",
}
starred_filter_true = {"field": "starred", "operator": "eq", "value": True}
starred_fetch_window_filter = {
"field": "creation_time",
"operator": "gte",
"value": 1705078800000,
}
_, outputs, _ = get_incidents_command(client, args)
handle_fetch_starred_mock.assert_called()
request_filters = handle_fetch_starred_mock.call_args.args[2]["filters"]
assert len(outputs["CoreApiModule.Incident(val.incident_id==obj.incident_id)"]) >= 1
assert starred_filter_true in request_filters
assert starred_fetch_window_filter in request_filters
assert outputs["CoreApiModule.Incident(val.incident_id==obj.incident_id)"][0]["starred"] is True
INPUT_test_handle_outgoing_issue_closure = load_test_data("./test_data/handle_outgoing_issue_closure_input.json")
@pytest.mark.parametrize(
"args, expected_delta",
[
# close an incident from xsoar ui, and the incident type isn't cortex xdr incident
(
INPUT_test_handle_outgoing_issue_closure["xsoar_ui_common_mapping"]["args"],
INPUT_test_handle_outgoing_issue_closure["xsoar_ui_common_mapping"]["expected_delta"],
),
# close an incident from xsoar ui, and the incident type is cortex xdr incident
(
INPUT_test_handle_outgoing_issue_closure["xsoar_ui_cortex_xdr_incident"]["args"],
INPUT_test_handle_outgoing_issue_closure["xsoar_ui_cortex_xdr_incident"]["expected_delta"],
),
# close an incident from XDR
(
INPUT_test_handle_outgoing_issue_closure["xdr"]["args"],
INPUT_test_handle_outgoing_issue_closure["xdr"]["expected_delta"],
),
],
)
def test_handle_outgoing_issue_closure(args, expected_delta):
"""
Given: An UpdateRemoteSystemArgs object.
- case A: data & delta that match a case of closing an incident from xsoar ui, and the incident type isn't cortex xdr incident
- case B: data & delta that match a case of closing an incident from xsoar ui, and the incident type is cortex xdr incident
- case C: data & delta that match a case of closing an incident from XDR.
When: Closing an incident.
Then: Ensure the update_args has the expected value.
- case A: a status is added with the correct value.
- case B: a status is added with the correct value.
- case C: a status isn't added. (If the closing status came from XDR, there is no need to update it again)
"""
from CommonServerPython import UpdateRemoteSystemArgs
remote_args = UpdateRemoteSystemArgs(args)
handle_outgoing_issue_closure(remote_args)
assert remote_args.delta == expected_delta
@pytest.mark.parametrize(
"custom_mapping, expected_resolved_status",
[
(
"Other=Other,Duplicate=Other,False Positive=False Positive,Resolved=True Positive",
[
"resolved_other",
"resolved_other",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
(
"Other=True Positive,Duplicate=Other,False Positive=False Positive,Resolved=True Positive",
[
"resolved_true_positive",
"resolved_other",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
(
"Duplicate=Other",
[
"resolved_other",
"resolved_other",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
# Expecting default mapping to be used When: no mapping provided.
(
"",
[
"resolved_other",
"resolved_duplicate",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
# Expecting default mapping to be used When: improper mapping is provided.
(
"Duplicate=RANDOM1, Other=Random2",
[
"resolved_other",
"resolved_duplicate",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
(
"Random1=Duplicate Incident",
[
"resolved_other",
"resolved_duplicate",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
# Expecting default mapping to be used When: improper mapping *format* is provided.
(
"Duplicate=Other False Positive=Other",
[
"resolved_other",
"resolved_duplicate",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
# Expecting default mapping to be used for When: improper key-value pair *format* is provided.
(
"Duplicate=Other, False Positive=Other True Positive=Other, Other=True Positive",
[
"resolved_true_positive",
"resolved_other",
"resolved_false_positive",
"resolved_true_positive",
"resolved_security_testing",
"resolved_other",
],
),
],
ids=[
"case-1",
"case-2",
"case-3",
"empty-case",
"improper-input-case-1",
"improper-input-case-2",
"improper-input-case-3",
"improper-input-case-4",
],
)
def test_xsoar_to_xdr_flexible_close_reason_mapping(capfd, mocker, custom_mapping, expected_resolved_status):
"""
Given:
- A custom XSOAR->XDR close-reason mapping
- Expected resolved XDR status according to the custom mapping.
When:
- Handling outgoing issue closure (handle_outgoing_issue_closure(...) executed).
Then:
- The resolved XDR statuses match the expected statuses for all possible XSOAR close-reasons.
"""
from CommonServerPython import UpdateRemoteSystemArgs
from CoreIRApiModule import handle_outgoing_issue_closure
mocker.patch.object(
demisto,
"params",
return_value={
"mirror_direction": "Both",
"custom_xsoar_to_xdr_close_reason_mapping": custom_mapping,
},
)
possible_xsoar_close_reasons = list(XSOAR_RESOLVED_STATUS_TO_XDR.keys()) + ["CUSTOM_CLOSE_REASON"]
for i, close_reason in enumerate(possible_xsoar_close_reasons):
remote_args = UpdateRemoteSystemArgs(
{
"delta": {"closeReason": close_reason},
"status": 2,
"inc_status": 2,
"data": {"status": "other"},
}
)
# Overcoming expected non-empty stderr test failures (Errors are submitted to stderr When: improper mapping is provided).
with capfd.disabled():
handle_outgoing_issue_closure(remote_args)
assert remote_args.delta.get("status")
assert remote_args.delta["status"] == expected_resolved_status[i]
@pytest.mark.parametrize(
"data, expected_result",
[
(
'{"reply": {"container": ["1.1.1.1"]}}',
{"reply": {"container": ["1.1.1.1"]}},
),
(b"XXXXXXX", b"XXXXXXX"),
],
)
def test_http_request_demisto_call(mocker, data, expected_result):
"""
Given:
- An XSIAM machine with a build version that supports demisto._apiCall() with RBAC validations.
When:
- Calling the http_request method.
Then:
- Make sure demisto._apiCall() is being called and the method returns the expected result.
- converting to json is possible - do it and return json
- converting to json is impossible - catch the error and return the data as is
"""
from CoreIRApiModule import CoreClient
client = CoreClient(
base_url=f"{Core_URL}/public_api/v1",
headers={},
)
mocker.patch("CoreIRApiModule.FORWARD_USER_RUN_RBAC", new=True)
mocker.patch.object(
demisto,
"_apiCall",
return_value={
"name": "/api/webapp/public_api/v1/distributions/get_versions/",
"status": 200,
"data": data,
},
)
res = client._http_request(method="POST", url_suffix="/distributions/get_versions/")
assert expected_result == res
@pytest.mark.parametrize("allow_bin_response", [True, False])
def test_request_for_bin_file_via_demisto_call(mocker, allow_bin_response):
"""
Given:
- An XSIAM machine with a build version that supports demisto._apiCall() with RBAC validations.
- case 1 - build version that support response of binary files.
- case 2 - build version that doesn't support response of binary files.
When:
- Calling the http_request method.
Then:
- case 1 - Make sure the response are as expected (base64 decoded).
- case 2 - Make sure en DemistoException was thrown with details about the server version that allowed bin response.
"""
import base64
from CoreIRApiModule import (
ALLOW_BIN_CONTENT_RESPONSE_BUILD_NUM,
ALLOW_BIN_CONTENT_RESPONSE_SERVER_VERSION,
CoreClient,
)
test_bin_data = b"test bin data"
client = CoreClient(
base_url=f"{Core_URL}/public_api/v1",
headers={},
)
mocker.patch("CoreIRApiModule.FORWARD_USER_RUN_RBAC", new=True)
mocker.patch("CoreIRApiModule.ALLOW_RESPONSE_AS_BINARY", new=allow_bin_response)
mocker.patch.object(
demisto,
"_apiCall",
return_value={
"name": "/api/webapp/public_api/v1/distributions/get_versions/",
"status": 200,
"data": base64.b64encode(test_bin_data),
},
)
try:
res = client._http_request(method="get", resp_type="content")
assert res == test_bin_data
except DemistoException as e:
assert f"{ALLOW_BIN_CONTENT_RESPONSE_SERVER_VERSION}-{ALLOW_BIN_CONTENT_RESPONSE_BUILD_NUM}" in str(e)
def test_terminate_process_command(mocker):
"""
Given:
- An XSIAM machine with a build version that supports demisto._apiCall() with RBAC validations.
- instance_id_1
- instance_id_2
- agent_id
When:
- Calling the terminate_process_command method.
Then:
- case 1 - Make sure the response are as expected (action_id).
"""
from CoreIRApiModule import CoreClient, terminate_process_command
client = CoreClient(
base_url=f"{Core_URL}/public_api/v1",
headers={},
)
mocker.patch("CoreIRApiModule.FORWARD_USER_RUN_RBAC", new=True)
mocker.patch.object(
demisto,
"_apiCall",
side_effect=[
{
"name": "/api/webapp/public_api/v1/endpoints/terminate_process",
"status": 200,
"data": json.dumps({"reply": {"group_action_id": 1}}),
},
{
"name": "/api/webapp/public_api/v1/endpoints/terminate_process",
"status": 200,
"data": json.dumps({"reply": {"group_action_id": 2}}),
},
],
)
result = terminate_process_command(
client=client,
args={"agent_id": "1", "instance_id": ["instance_id_1", "instance_id_2"]},
)
assert result.readable_output == (
"### Action terminate process created on instance ids:"
" instance_id_1, instance_id_2\n|action_id|\n|---|\n| 1 |\n| 2 |\n"
)
assert result.raw_response == [{"action_id": 1}, {"action_id": 2}]
def test_terminate_causality_command(mocker):
"""
Given:
- An XSIAM machine with a build version that supports demisto._apiCall() with RBAC validations.
- causality_id
- agent_id
When:
- Calling the terminate_causality_command method.
Then:
- case 1 - Make sure the response are as expected (action_id).
"""
from CoreIRApiModule import CoreClient, terminate_causality_command
client = CoreClient(
base_url=f"{Core_URL}/public_api/v1",
headers={},
)
mocker.patch("CoreIRApiModule.FORWARD_USER_RUN_RBAC", new=True)
mocker.patch.object(
demisto,
"_apiCall",
side_effect=[
{
"name": "/api/webapp/public_api/v1/endpoints/terminate_causality",
"status": 200,
"data": json.dumps({"reply": {"group_action_id": 1}}),
},
{
"name": "/api/webapp/public_api/v1/endpoints/terminate_causality",
"status": 200,
"data": json.dumps({"reply": {"group_action_id": 2}}),
},
],
)
result = terminate_causality_command(
client=client,
args={"agent_id": "1", "causality_id": ["causality_id_1", "causality_id_2"]},
)
assert result.readable_output == (
"### Action terminate causality created on causality_id_1,causality_id_2\n|action_id|\n|---|\n| 1 |\n| 2 |\n"
)
assert result.raw_response == [{"action_id": 1}, {"action_id": 2}]
def test_run_polling_command_values_raise_error(mocker):
"""
Given -
- run_polling_command arguments.
When: -
- Running the run_polling_command
Then:
- Make sure that an error is raised with the correct output.
"""
from unittest.mock import Mock
from CommonServerPython import DemistoException, ScheduledCommand
from CoreIRApiModule import run_polling_command
polling_args = {
"endpoint_ids": "1",
"command_decision_field": "action_id",
"action_id": "1",
"hide_polling_output": True,
}
mocker.patch.object(ScheduledCommand, "raise_error_if_not_supported", return_value=None)
client = Mock()
mock_command_results = Mock()
mock_command_results.raw_response = {"status": "TIMEOUT"}
mock_command_results.return_value = mock_command_results
client.get_command_results.return_value = mock_command_results
mocker.patch("CoreIRApiModule.return_results")
with pytest.raises(DemistoException) as e:
run_polling_command(
client=client,
args=polling_args,
cmd="core-terminate-causality",
command_function=Mock(),
command_decision_field="action_id",
results_function=mock_command_results,
polling_field="status",
polling_value=["PENDING", "IN_PROGRESS", "PENDING_ABORT"],
values_raise_error=["FAILED", "TIMEOUT", "ABORTED", "CANCELED"],
)
assert str(e.value) == "The command core-terminate-causality failed. Received status TIMEOUT"
@pytest.mark.parametrize(
"exception_instance, command, expected_result",
[
(
DemistoException(
"An error occurred while processing XDR public API: No identity threat",
res=Mock(status_code=500),
),
"user",
(
"Please confirm the XDR Identity Threat Module is enabled.\nFull error message: "
"An error occurred while processing XDR public API: No identity threat"
),
),
(
Exception('"err_code": 500: No identity threat. An error occurred while processing XDR public API'),
"user",
(
'Please confirm the XDR Identity Threat Module is enabled.\nFull error message: "err_code": '
"500: No identity threat. An error occurred while processing XDR public API"
),
),
(
DemistoException("500: The id 'test_user' was not found", res=Mock(status_code=500)),
"user",
"The user test_user was not found",
),
(
Exception("\"err_code\": 500: The id 'test_user' was not found"),
"user",
"The user test_user was not found",
),
(DemistoException("Some other error", res=Mock(status_code=500)), "user", None),
(Exception("Some other error"), "user", None),
(
DemistoException(
"An error occurred while processing XDR public API: No identity threat",
res=Mock(status_code=500),
),
"host",
(
"Please confirm the XDR Identity Threat Module is enabled.\nFull error message:"
" An error occurred while processing XDR public API: No identity threat"
),
),
(
Exception('"err_code": 500: No identity threat. An error occurred while processing XDR public API'),
"host",
(
'Please confirm the XDR Identity Threat Module is enabled.\nFull error message: "err_code":'
" 500: No identity threat. An error occurred while processing XDR public API"
),
),
(DemistoException("Some other error", res=Mock(status_code=500)), "host", None),
(Exception("Some other error"), "host", None),
(
DemistoException("500: The id 'test_host' was not found", res=Mock(status_code=500)),
"host",
"The host test_host was not found",
),
(
Exception("\"err_code\": 500: The id 'test_host' was not found"),
"host",
"The host test_host was not found",
),
],
)
def test_list_risky_users_or_host_command(exception_instance, command, expected_result):
"""
Given -
- Test case 1: raises DemistoException with user command, where the error is related to the XDR Identity Threat module
being disabled.
- Test case 2: raises Exception with user command, where the error is related to the XDR Identity Threat module being
disabled.
- Test case 3: raises DemistoException with user command, where the error is related to a missing user
(based on the provided id).
- Test case 4: raises Exception with user command, where the error is related to a missing user
(based on the provided id).
- Test case 5: raises DemistoException with user command, where the error is unrelated and no warning is expected.
- Test case 6: Exception with user command, where the error is unrelated and no warning is expected.
- Test case 7: raises DemistoException with host command, where the error is related to the XDR Identity Threat module
being
disabled.
- Test case 8: raises Exception with host command, where the error is related to the XDR Identity Threat module being
disabled.
- Test case 9: raises DemistoException with host command, where the error is unrelated and no warning is expected.
- Test case 10: raises Exception with host command, where the error is unrelated and no warning is expected.
- Test case 11: raises DemistoException with host command, where the error is related to a missing host
(based on the provided id).
- Test case 12: raises Exception with host command, where the error is related to a missing host
(based on the provided id).
When: -
- The function `list_risky_users_or_host_command` is called with either a "user" or "host" command, and it encounters
the provided exception instance. The function attempts to handle the exception and generate a warning if needed.
Then: -
- The function should either trigger a warning or not trigger a warning, based on the type of exception and the expected
result.
The function's output should match the expected result:
- If the error is related to the XDR module or missing resource, the warning message should be returned.
- If the error is unrelated, no warning should be triggered.
"""
client = Mock()
args = {"user_id": "test_user"} if command == "user" else {"host_id": "test_host"}
client.risk_score_user_or_host.side_effect = exception_instance
with patch("CoreIRApiModule.return_warning") as mock_return_warning:
try:
result = list_risky_users_or_host_command(client, command, args)
assert result.readable_output == expected_result
except Exception:
if expected_result:
mock_return_warning.assert_called_once_with(expected_result, exit=True)
else:
mock_return_warning.assert_not_called()
def test_get_alert_by_filter_custom_filter_valid_json(requests_mock):
"""
Given:
- Core client
- Valid JSON custom_filter with agent_id
When:
- Running get_alerts_by_filter command
Then:
- Verify the JSON is parsed correctly without any fixes applied
"""
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
# Valid JSON with agent_id
custom_filter = '{"AND":[{"SEARCH_FIELD": "agent_id", "SEARCH_TYPE": "CONTAINS", "SEARCH_VALUE": "1.2.3.4"}]}'
args = {"custom_filter": custom_filter}
response = get_alerts_by_filter_command(client, args)
assert response.outputs[0].get("internal_id", {}) == 33333
def test_get_alert_by_filter_custom_filter_malformed_json_fixed(requests_mock):
"""
Given:
- Core client
- Malformed JSON custom_filter with agent_id containing array-like string values
When:
- Running get_alerts_by_filter command
Then:
- Verify the malformed JSON is automatically fixed and parsed correctly
"""
from CoreIRApiModule import CoreClient, get_alerts_by_filter_command
api_response = load_test_data("./test_data/get_alerts_by_filter_results.json")
requests_mock.post(f"{Core_URL}/public_api/v1/alerts/get_alerts_by_filter_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/public_api/v1", headers={})
# Malformed JSON with agent_id - array values as string with unescaped quotes
custom_filter = '{"AND":[{"SEARCH_FIELD": "agent_id", "SEARCH_TYPE": "CONTAINS", "SEARCH_VALUE": "[1.2.3.4, 5.6.7.8]"}]}'
args = {"custom_filter": custom_filter}
response = get_alerts_by_filter_command(client, args)
assert response.outputs[0].get("internal_id", {}) == 33333
def test_isolate_endpoint_disconnected_with_suppress_enabled(mocker):
"""
Given:
- An endpoint with status DISCONNECTED
- suppress_disconnected_endpoint_error is True
When:
- Calling isolate_endpoint_command
Then:
- The client.isolate_endpoint method is called (no error is raised)
- A warning message is returned
"""
from CoreIRApiModule import isolate_endpoint_command
# Mock the get_endpoint API to return a disconnected endpoint
mocker.patch.object(
test_client,
"_http_request",
side_effect=[
{"reply": {"endpoints": [{"endpoint_id": "1111", "endpoint_status": "DISCONNECTED"}]}},
{"reply": {"action_id": "fake_action_id"}}, # mock for isolate_endpoint
],
)
mocker.patch.object(test_client, "isolate_endpoint", return_value={"action_id": "fake_action_id"})
args = {"endpoint_id": "1111", "suppress_disconnected_endpoint_error": True}
result = isolate_endpoint_command(test_client, args)
assert result.readable_output == "Warning: isolation action is pending for the following disconnected endpoint: 1111."
def test_create_filter_from_args():
"""
Test case to verify the filter creation logic based on input arguments
"""
from CoreIRApiModule import create_filter_from_args
# Default test case with valid inputs
args = {"alert_id": "test_1, test_2, test_3", "not_status": "In Progress, New"}
query = create_filter_from_args(args)
expected_result = {
"AND": [
{
"OR": [
{
"SEARCH_FIELD": "internal_id",
"SEARCH_TYPE": "EQ",
"SEARCH_VALUE": "test_1",
},
{
"SEARCH_FIELD": "internal_id",
"SEARCH_TYPE": "EQ",
"SEARCH_VALUE": "test_2",
},
{
"SEARCH_FIELD": "internal_id",
"SEARCH_TYPE": "EQ",
"SEARCH_VALUE": "test_3",
},
]
},
{
"AND": [
{
"SEARCH_FIELD": "status.progress",
"SEARCH_TYPE": "NEQ",
"SEARCH_VALUE": "STATUS_020_UNDER_INVESTIGATION",
},
{
"SEARCH_FIELD": "status.progress",
"SEARCH_TYPE": "NEQ",
"SEARCH_VALUE": "STATUS_010_NEW",
},
]
},
]
}
assert expected_result == query
class TestFilterBuilder:
def test_add_field_without_mapper(self):
"""
Given:
A FilterBuilder instance and field parameters without a mapper.
When:
The add_field method is called with name, type, and values.
Then:
A new Field should be added to filter_fields with the original values.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
values = ["value1", "value2"]
filter_builder.add_field("test_field", FilterType.EQ, values)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert field.field_name == "test_field"
assert field.filter_type == FilterType.EQ
assert field.values == values
def test_add_field_with_mapper_list_values(self):
"""
Given:
A FilterBuilder instance, field parameters with a mapper, and list values.
When:
The add_field method is called with values that exist in the mapper.
Then:
A new Field should be added with mapped values only.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
values = ["low", "high", "unknown"]
mapper = {"low": "SEV_040_LOW", "high": "SEV_060_HIGH"}
filter_builder.add_field("severity", FilterType.EQ, values, mapper)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert field.field_name == "severity"
assert field.filter_type == FilterType.EQ
assert field.values == ["SEV_040_LOW", "SEV_060_HIGH"]
def test_add_field_with_mapper_single_value(self):
"""
Given:
A FilterBuilder instance, field parameters with a mapper, and a single value.
When:
The add_field method is called with a single value that exists in the mapper.
Then:
The single value should be converted to a list and mapped correctly.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
value = "medium"
mapper = {"medium": "SEV_050_MEDIUM", "high": "SEV_060_HIGH"}
filter_builder.add_field("severity", FilterType.EQ, value, mapper)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert field.field_name == "severity"
assert field.filter_type == FilterType.EQ
assert field.values == ["SEV_050_MEDIUM"]
def test_add_field_with_mapper_no_matching_values(self):
"""
Given:
A FilterBuilder instance, field parameters with a mapper, and values not in the mapper.
When:
The add_field method is called with values that don't exist in the mapper.
Then:
A new Field should be added with an empty list of processed values.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
values = ["unknown", "invalid"]
mapper = {"low": "SEV_040_LOW", "high": "SEV_060_HIGH"}
filter_builder.add_field("severity", FilterType.EQ, values, mapper)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert field.field_name == "severity"
assert field.filter_type == FilterType.EQ
assert field.values == []
def test_add_field_with_mappings_single_mapped_value(self):
"""
Given: A FilterBuilder instance and a single mapped value that exists in the mappings dictionary.
When: The add_field_with_mappings method is called with a mapped value.
Then: A MappedValuesField should be added to the filter_fields list with the correct parameters.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
mappings = {
"unassigned": FilterBuilder.FilterType.IS_EMPTY,
"assigned": FilterBuilder.FilterType.NIS_EMPTY,
}
filter_builder.add_field_with_mappings("assignee", FilterBuilder.FilterType.CONTAINS, "unassigned", mappings)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert isinstance(field, FilterBuilder.MappedValuesField)
assert field.field_name == "assignee"
assert field.filter_type == FilterBuilder.FilterType.CONTAINS
assert field.values == "unassigned"
assert field.mappings == mappings
def test_add_field_with_mappings_multiple_mapped_values(self):
"""
Given: A FilterBuilder instance and multiple values that exist in the mappings dictionary.
When: The add_field_with_mappings method is called with a list of mapped values.
Then: A MappedValuesField should be added with the list of values and correct mappings.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
mappings = {
"unassigned": FilterBuilder.FilterType.IS_EMPTY,
"assigned": FilterBuilder.FilterType.NIS_EMPTY,
"pending": FilterBuilder.FilterType.CONTAINS,
}
values = ["unassigned", "assigned"]
filter_builder.add_field_with_mappings("status", FilterBuilder.FilterType.EQ, values, mappings)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert isinstance(field, FilterBuilder.MappedValuesField)
assert field.field_name == "status"
assert field.filter_type == FilterBuilder.FilterType.EQ
assert field.values == values
assert field.mappings == mappings
def test_add_field_with_mappings_unmapped_value(self):
"""
Given: A FilterBuilder instance and a value that does not exist in the mappings dictionary.
When: The add_field_with_mappings method is called with an unmapped value.
Then: A MappedValuesField should be added with the default filter type for unmapped values.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
mappings = {
"unassigned": FilterBuilder.FilterType.IS_EMPTY,
"assigned": FilterBuilder.FilterType.NIS_EMPTY,
}
filter_builder.add_field_with_mappings("assignee", FilterBuilder.FilterType.CONTAINS, "john.doe", mappings)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert isinstance(field, FilterBuilder.MappedValuesField)
assert field.field_name == "assignee"
assert field.filter_type == FilterBuilder.FilterType.CONTAINS
assert field.values == "john.doe"
assert field.mappings == mappings
def test_add_field_with_mappings_mixed_values(self):
"""
Given: A FilterBuilder instance and a list containing both mapped and unmapped values.
When: The add_field_with_mappings method is called with mixed value types.
Then: A MappedValuesField should be added containing all values with their respective mappings.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
mappings = {
"unassigned": FilterBuilder.FilterType.IS_EMPTY,
"assigned": FilterBuilder.FilterType.NIS_EMPTY,
}
values = ["unassigned", "john.doe", "assigned"]
filter_builder.add_field_with_mappings("assignee", FilterBuilder.FilterType.CONTAINS, values, mappings)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert isinstance(field, FilterBuilder.MappedValuesField)
assert field.field_name == "assignee"
assert field.filter_type == FilterBuilder.FilterType.CONTAINS
assert field.values == values
assert field.mappings == mappings
def test_add_field_with_mappings_empty_mappings(self):
"""
Given: A FilterBuilder instance and an empty mappings dictionary.
When: The add_field_with_mappings method is called with empty mappings.
Then: A MappedValuesField should be added with the empty mappings dictionary.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
mappings = {}
filter_builder.add_field_with_mappings("field", FilterBuilder.FilterType.EQ, "value", mappings)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert isinstance(field, FilterBuilder.MappedValuesField)
assert field.field_name == "field"
assert field.filter_type == FilterBuilder.FilterType.EQ
assert field.values == "value"
assert field.mappings == {}
def test_add_field_with_mappings_none_value(self):
"""
Given: A FilterBuilder instance and None as the value parameter.
When: The add_field_with_mappings method is called with None value.
Then: A MappedValuesField should be added with None as the values.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
mappings = {
"unassigned": FilterBuilder.FilterType.IS_EMPTY,
}
filter_builder.add_field_with_mappings("assignee", FilterBuilder.FilterType.CONTAINS, None, mappings)
assert len(filter_builder.filter_fields) == 1
field = filter_builder.filter_fields[0]
assert isinstance(field, FilterBuilder.MappedValuesField)
assert field.field_name == "assignee"
assert field.filter_type == FilterBuilder.FilterType.CONTAINS
assert field.values is None
assert field.mappings == mappings
def test_add_time_range_field_with_valid_start_and_end_time(self, mocker: MockerFixture):
"""
Given: A FilterBuilder instance and valid start_time and end_time strings.
When: add_time_range_field is called with both start and end times.
Then: The method should add a RANGE field with from and to values to the filter.
"""
from CoreIRApiModule import FilterBuilder, FilterType
# Arrange
filter_builder = FilterBuilder()
mock_prepare_time_range = mocker.patch.object(
filter_builder,
"_prepare_time_range",
return_value=(1640995200000, 1641081600000),
)
mock_add_field = mocker.patch.object(filter_builder, "add_field")
# Act
filter_builder.add_time_range_field("test_field", "2022-01-01T00:00:00", "2022-01-02T00:00:00")
# Assert
mock_prepare_time_range.assert_called_once_with("2022-01-01T00:00:00", "2022-01-02T00:00:00")
mock_add_field.assert_called_once_with("test_field", FilterType.RANGE, {"from": 1640995200000, "to": 1641081600000})
def test_add_time_range_field_with_none_start_time(self, mocker: MockerFixture):
"""
Given: A FilterBuilder instance with None start_time and valid end_time.
When: add_time_range_field is called with start_time as None.
Then: The method should not add any field to the filter since start is None.
"""
from CoreIRApiModule import FilterBuilder, FilterType
# Arrange
filter_builder = FilterBuilder()
mock_prepare_time_range = mocker.patch.object(filter_builder, "_prepare_time_range", return_value=(0, 1641081600000))
mock_add_field = mocker.patch.object(filter_builder, "add_field")
# Act
filter_builder.add_time_range_field("test_field", None, "2022-01-02T00:00:00")
# Assert
mock_prepare_time_range.assert_called_once_with(None, "2022-01-02T00:00:00")
mock_add_field.assert_called_once_with("test_field", FilterType.RANGE, {"from": 0, "to": 1641081600000})
def test_add_time_range_field_with_none_end_time(self, mocker: MockerFixture):
"""
Given: A FilterBuilder instance with valid start_time and None end_time.
When: add_time_range_field is called with end_time as None.
Then: The method should not add any field to the filter since end is None.
"""
from CoreIRApiModule import FilterBuilder
# Arrange
filter_builder = FilterBuilder()
mock_prepare_time_range = mocker.patch.object(filter_builder, "_prepare_time_range", return_value=(1640995200000, None))
mock_add_field = mocker.patch.object(filter_builder, "add_field")
# Act
filter_builder.add_time_range_field("test_field", "2022-01-01T00:00:00", None)
# Assert
mock_prepare_time_range.assert_called_once_with("2022-01-01T00:00:00", None)
mock_add_field.assert_not_called()
def test_add_time_range_field_with_both_none_times(self, mocker: MockerFixture):
"""
Given: A FilterBuilder instance with both start_time and end_time as None.
When: add_time_range_field is called with both times as None.
Then: The method should not add any field to the filter since both values are None.
"""
from CoreIRApiModule import FilterBuilder
# Arrange
filter_builder = FilterBuilder()
mock_prepare_time_range = mocker.patch.object(filter_builder, "_prepare_time_range", return_value=(None, None))
mock_add_field = mocker.patch.object(filter_builder, "add_field")
# Act
filter_builder.add_time_range_field("test_field", None, None)
# Assert
mock_prepare_time_range.assert_called_once_with(None, None)
mock_add_field.assert_not_called()
def test_to_dict_empty_filter_fields(self):
"""
Given: A FilterBuilder instance with no filter fields.
When: The to_dict method is called.
Then: An empty dictionary should be returned.
"""
from CoreIRApiModule import FilterBuilder
filter_builder = FilterBuilder()
result = filter_builder.to_dict()
assert result == {}
def test_to_dict_single_field_single_value(self):
"""
Given: A FilterBuilder with one field containing a single non-list value.
When: The to_dict method is called.
Then: A properly structured filter dictionary with one search object should be returned.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
filter_builder.add_field("test_field", FilterType.EQ, "test_value")
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "test_field",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "test_value",
}
]
}
assert result == expected
def test_to_dict_single_field_multiple_values(self):
"""
Given: A FilterBuilder with one field containing multiple values in a list.
When: The to_dict method is called.
Then: A filter dictionary with OR operator grouping multiple search values should be returned.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
filter_builder.add_field("test_field", FilterType.EQ, ["value1", "value2"])
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterType.EQ.operator: [
{
FilterBuilder.FIELD: "test_field",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "value1",
},
{
FilterBuilder.FIELD: "test_field",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "value2",
},
]
}
]
}
assert result == expected
def test_to_dict_multiple_fields(self):
"""
Given: A FilterBuilder with multiple fields each containing different values.
When: The to_dict method is called.
Then: A filter dictionary with AND operator containing all field filters should be returned.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
filter_builder.add_field("field1", FilterType.EQ, "value1")
filter_builder.add_field("field2", FilterType.CONTAINS, "value2")
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "field1",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "value1",
},
{
FilterBuilder.FIELD: "field2",
FilterBuilder.TYPE: FilterType.CONTAINS.value,
FilterBuilder.VALUE: "value2",
},
]
}
assert result == expected
def test_to_dict_with_none_values_filtered_out(self):
"""
Given: A FilterBuilder with fields containing None values mixed with valid values.
When: The to_dict method is called.
Then: None values should be filtered out and only valid values should appear in the result.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
filter_builder.add_field("test_field", FilterType.EQ, [None, "valid_value", None])
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "test_field",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "valid_value",
}
]
}
assert result == expected
def test_to_dict_with_all_none_values(self):
"""
Given: A FilterBuilder with fields containing only None values.
When: The to_dict method is called.
Then: An empty dictionary should be returned since all values are filtered out.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
filter_builder.add_field("test_field", FilterType.EQ, [None, None])
result = filter_builder.to_dict()
assert result == {}
def test_to_dict_with_mapped_values_field_normal_value(self):
"""
Given: A MappedValuesField with a value that is not in the mappings dictionary.
When: The to_dict method is called.
Then: The default filter type should be used for the unmapped value.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
mappings = {"special": FilterType.IS_EMPTY}
filter_builder.add_field_with_mappings("test_field", FilterType.EQ, "normal_value", mappings)
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "test_field",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "normal_value",
}
]
}
assert result == expected
def test_to_dict_with_mapped_values_field_is_empty(self):
"""
Given: A MappedValuesField with a value mapped to IS_EMPTY filter type.
When: The to_dict method is called.
Then: The mapped filter type should be used and value should be set to "<No Value>".
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
mappings = {"unassigned": FilterType.IS_EMPTY}
filter_builder.add_field_with_mappings("assignee", FilterType.EQ, "unassigned", mappings)
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "assignee",
FilterBuilder.TYPE: FilterType.IS_EMPTY.value,
FilterBuilder.VALUE: "<No Value>",
}
]
}
assert result == expected
def test_to_dict_with_mapped_values_field_nis_empty(self):
"""
Given: A MappedValuesField with a value mapped to NIS_EMPTY filter type.
When: The to_dict method is called.
Then: The mapped filter type should be used and value should be set to "<No Value>".
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
mappings = {"assigned": FilterType.NIS_EMPTY}
filter_builder.add_field_with_mappings("assignee", FilterType.EQ, "assigned", mappings)
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "assignee",
FilterBuilder.TYPE: FilterType.NIS_EMPTY.value,
FilterBuilder.VALUE: "<No Value>",
}
]
}
assert result == expected
def test_to_dict_with_mixed_mapped_and_normal_values(self):
"""
Given: A MappedValuesField with both mapped and unmapped values in the same field.
When: The to_dict method is called.
Then: Each value should use its appropriate filter type and the results should be grouped with OR operator.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
mappings = {"unassigned": FilterType.IS_EMPTY}
filter_builder.add_field_with_mappings("assignee", FilterType.EQ, ["unassigned", "john.doe"], mappings)
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterType.EQ.operator: [
{
FilterBuilder.FIELD: "assignee",
FilterBuilder.TYPE: FilterType.IS_EMPTY.value,
FilterBuilder.VALUE: "<No Value>",
},
{
FilterBuilder.FIELD: "assignee",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "john.doe",
},
]
}
]
}
assert result == expected
def test_to_dict_converts_non_list_values_to_list(self):
"""
Given: A FilterBuilder with field values that are not initially in list format.
When: The to_dict method is called.
Then: The non-list values should be converted to lists internally for processing.
"""
from CoreIRApiModule import FilterBuilder, FilterType
filter_builder = FilterBuilder()
# Directly create a field with non-list value
field = FilterBuilder.Field("test_field", FilterType.EQ, "single_value")
filter_builder.filter_fields = [field]
result = filter_builder.to_dict()
expected = {
FilterBuilder.AND: [
{
FilterBuilder.FIELD: "test_field",
FilterBuilder.TYPE: FilterType.EQ.value,
FilterBuilder.VALUE: "single_value",
}
]
}
assert result == expected
def test_prepare_time_range_both_valid_times(self, mocker: MockerFixture):
"""
Given: Valid start_time and end_time strings that can be parsed by dateparser.
When: _prepare_time_range is called with both valid time strings.
Then: Both timestamps should be converted to milliseconds and returned as a tuple.
"""
from CoreIRApiModule import FilterBuilder
from datetime import datetime
# Mock dateparser.parse to return known datetime objects
start_dt = datetime(2023, 1, 1, 10, 0, 0)
end_dt = datetime(2023, 1, 2, 15, 30, 0)
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse")
mock_parse.side_effect = [start_dt, end_dt]
start_time, end_time = FilterBuilder._prepare_time_range("2023-01-01T10:00:00", "2023-01-02T15:30:00")
assert start_time == int(start_dt.timestamp() * 1000)
assert end_time == int(end_dt.timestamp() * 1000)
assert mock_parse.call_count == 2
def test_prepare_time_range_only_start_time_provided(self, mocker: MockerFixture):
"""
Given: A valid start_time string and None as end_time.
When: _prepare_time_range is called with only start_time provided.
Then: start_time should be converted to milliseconds and end_time should be set to current time.
"""
from CoreIRApiModule import FilterBuilder
from datetime import datetime
# Mock dateparser.parse for start_time
start_dt = datetime(2023, 1, 1, 10, 0, 0)
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse", return_value=start_dt)
# Mock datetime.now for end_time calculation
current_dt = datetime(2023, 1, 3, 12, 0, 0)
mock_now = mocker.patch("CoreIRApiModule.datetime")
mock_now.now.return_value = current_dt
start_time, end_time = FilterBuilder._prepare_time_range("2023-01-01T10:00:00", None)
assert start_time == int(start_dt.timestamp() * 1000)
assert end_time == int(current_dt.timestamp() * 1000)
mock_parse.assert_called_once_with("2023-01-01T10:00:00")
def test_prepare_time_range_both_none_times(self):
"""
Given: Both start_time_str and end_time_str parameters as None.
When: _prepare_time_range is called with both parameters as None.
Then: Both returned timestamps should be None without any parsing attempts.
"""
from CoreIRApiModule import FilterBuilder
start_time, end_time = FilterBuilder._prepare_time_range(None, None)
assert start_time is None
assert end_time is None
def test_prepare_time_range_invalid_start_time_raises_value_error(self, mocker: MockerFixture):
"""
Given: An invalid start_time string that cannot be parsed by dateparser.
When: _prepare_time_range is called with an unparseable start_time.
Then: A ValueError should be raised with the invalid start_time in the error message.
"""
from CoreIRApiModule import FilterBuilder
# Mock dateparser.parse to return None for invalid input
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse", return_value=None)
with pytest.raises(ValueError, match="Could not parse start_time: invalid_start_time"):
FilterBuilder._prepare_time_range("invalid_start_time", None)
mock_parse.assert_called_once_with("invalid_start_time")
def test_prepare_time_range_invalid_end_time_raises_value_error(self, mocker: MockerFixture):
"""
Given: A valid start_time and an invalid end_time string that cannot be parsed.
When: _prepare_time_range is called with valid start_time but unparseable end_time.
Then: A ValueError should be raised with the invalid end_time in the error message.
"""
from CoreIRApiModule import FilterBuilder
from datetime import datetime
# Mock dateparser.parse to return valid datetime for start_time and None for end_time
start_dt = datetime(2023, 1, 1, 10, 0, 0)
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse")
mock_parse.side_effect = [start_dt, None]
with pytest.raises(ValueError, match="Could not parse end_time: invalid_end_time"):
FilterBuilder._prepare_time_range("2023-01-01T10:00:00", "invalid_end_time")
assert mock_parse.call_count == 2
def test_prepare_time_range_string_conversion_for_start_time(self, mocker: MockerFixture):
"""
Given: A non-string start_time parameter that needs string conversion.
When: _prepare_time_range is called with start_time that requires str() conversion.
Then: The start_time should be converted to string before parsing and processed correctly.
"""
from CoreIRApiModule import FilterBuilder
from datetime import datetime
# Mock dateparser.parse to return a valid datetime
start_dt = datetime(2023, 1, 1, 10, 0, 0)
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse", return_value=start_dt)
# Pass an integer that should be converted to string
start_time, end_time = FilterBuilder._prepare_time_range(20230101, None)
# Verify that str() was called on the parameter
mock_parse.assert_called_with("20230101")
assert start_time == int(start_dt.timestamp() * 1000)
def test_prepare_time_range_string_conversion_for_end_time(self, mocker: MockerFixture):
"""
Given: A non-string end_time parameter along with valid start_time.
When: _prepare_time_range is called with end_time that requires str() conversion.
Then: The end_time should be converted to string before parsing and both times processed correctly.
"""
from CoreIRApiModule import FilterBuilder
from datetime import datetime
# Mock dateparser.parse to return valid datetimes
start_dt = datetime(2023, 1, 1, 10, 0, 0)
end_dt = datetime(2023, 1, 2, 15, 30, 0)
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse")
mock_parse.side_effect = [start_dt, end_dt]
# Pass integers that should be converted to strings
start_time, end_time = FilterBuilder._prepare_time_range(20230101, 20230102)
# Verify that str() was called on both parameters
assert mock_parse.call_args_list[0][0][0] == "20230101"
assert mock_parse.call_args_list[1][0][0] == "20230102"
assert start_time == int(start_dt.timestamp() * 1000)
assert end_time == int(end_dt.timestamp() * 1000)
def test_prepare_time_range_millisecond_conversion_precision(self, mocker: MockerFixture):
"""
Given: Valid datetime objects returned from dateparser with specific timestamp values.
When: _prepare_time_range converts the timestamps to milliseconds.
Then: The conversion should multiply by 1000 and convert to integer with correct precision.
"""
from CoreIRApiModule import FilterBuilder
from datetime import datetime
# Create datetime with known timestamp
start_dt = datetime(2023, 1, 1, 10, 0, 0)
end_dt = datetime(2023, 1, 2, 15, 30, 0)
mock_parse = mocker.patch("CoreIRApiModule.dateparser.parse")
mock_parse.side_effect = [start_dt, end_dt]
start_time, end_time = FilterBuilder._prepare_time_range("2023-01-01T10:00:00", "2023-01-02T15:30:00")
# Verify precise millisecond conversion
expected_start = int(start_dt.timestamp() * 1000)
expected_end = int(end_dt.timestamp() * 1000)
assert start_time == expected_start
assert end_time == expected_end
assert isinstance(start_time, int)
assert isinstance(end_time, int)
class TestDetermineEmailOrName:
def test_determine_email_or_name_empty_list(self):
"""
Given: An empty list is passed to determine_email_or_name.
When: The function is called with an empty assignee list.
Then: Should return an empty string.
"""
from CoreIRApiModule import determine_email_or_name
result = determine_email_or_name([])
assert result == ""
def test_determine_email_or_name_with_valid_email(self):
"""
Given: A list containing a valid email address.
When: determine_email_or_name is called with an email in the assignee list.
Then: Should return "email" indicating the input format is email.
"""
from CoreIRApiModule import determine_email_or_name
assignee_list = ["user@example.com"]
result = determine_email_or_name(assignee_list)
assert result == "email"
def test_determine_email_or_name_with_username(self):
"""
Given: A list containing a username (non-email format).
When: determine_email_or_name is called with a username in the assignee list.
Then: Should return "name" indicating the input format is a username.
"""
from CoreIRApiModule import determine_email_or_name
assignee_list = ["john_doe"]
result = determine_email_or_name(assignee_list)
assert result == "name"
def test_get_issues_by_filter_custom_filter_valid_json(requests_mock):
"""
Given:
- Core client
- Valid JSON custom_filter with agent_id
When:
- Running get_alerts_by_filter command
Then:
- Verify the JSON is parsed correctly without any fixes applied
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = load_test_data("./test_data/get_issues_by_filter_results.json")
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
# Valid JSON with agent_id
custom_filter = '{"AND":[{"SEARCH_FIELD": "agent_id", "SEARCH_TYPE": "CONTAINS", "SEARCH_VALUE": "1.2.3.4"}]}'
args = {"custom_filter": custom_filter}
response = get_issues_by_filter_command(client, args)
assert response[0].outputs[0].get("internal_id", {}) == 33333
def test_get_issues_by_filter_custom_filter_malformed_json_fixed(requests_mock):
"""
Given:
- Core client
- Malformed JSON custom_filter with agent_id containing array-like string values
When:
- Running get_alerts_by_filter command
Then:
- Verify the malformed JSON is automatically fixed and parsed correctly
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = load_test_data("./test_data/get_issues_by_filter_results.json")
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
# Malformed JSON with agent_id - array values as string with unescaped quotes
custom_filter = '{"AND":[{"SEARCH_FIELD": "agent_id", "SEARCH_TYPE": "CONTAINS", "SEARCH_VALUE": "[1.2.3.4, 5.6.7.8]"}]}'
args = {"custom_filter": custom_filter}
response = get_issues_by_filter_command(client, args)
assert response[0].outputs[0].get("internal_id", {}) == 33333
# ---------------------------------------------------------------------------
# BIOC description rendering (render_bioc_description) + BIOC-only filtering
# ---------------------------------------------------------------------------
def test_render_bioc_description_simple_attribute_operator_value():
"""
Given: A structured BIOC indicator with a single attribute = value clause.
When: render_bioc_description is called.
Then: The plain text mirrors the UI ("<attr> <op> <value>").
"""
from CoreIRApiModule import render_bioc_description
indicator = [
{"render_type": "attribute", "pretty_name": "Action File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "evil.exe"},
]
assert render_bioc_description(indicator) == "Action File Name = evil.exe"
def test_render_bioc_description_entity_with_brackets_and_and():
"""
Given: Two entities each with an attribute clause.
When: render_bioc_description is called.
Then: AND connector and [ ] brackets are inserted like the UI.
"""
from CoreIRApiModule import render_bioc_description
indicator = [
{"render_type": "entity", "pretty_name": "Actor Process"},
{"render_type": "attribute", "pretty_name": "Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "a.exe"},
{"render_type": "entity", "pretty_name": "Causality Actor"},
{"render_type": "attribute", "pretty_name": "Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "b.exe"},
]
result = render_bioc_description(indicator)
assert "AND" in result
assert "[" in result
assert "]" in result
assert "a.exe" in result
assert "b.exe" in result
def test_render_bioc_description_or_group_parentheses():
"""
Given: An indicator containing an OR connector.
When: render_bioc_description is called.
Then: The OR group is wrapped in parentheses (addParenthesisMetadata).
"""
from CoreIRApiModule import render_bioc_description
indicator = [
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "a.exe"},
{"render_type": "connector", "pretty_name": "OR"},
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "b.exe"},
]
result = render_bioc_description(indicator)
assert "OR" in result
assert "(" in result
assert ")" in result
def test_render_bioc_description_does_not_mutate_input():
"""
Given: A structured BIOC indicator.
When: render_bioc_description is called.
Then: The original input list is not mutated (parenthesis metadata applied to a copy).
"""
from CoreIRApiModule import render_bioc_description
indicator = [
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "a.exe"},
{"render_type": "connector", "pretty_name": "OR"},
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "b.exe"},
]
snapshot = json.loads(json.dumps(indicator))
render_bioc_description(indicator)
assert indicator == snapshot
def test_get_issues_by_filter_renders_bioc_description(requests_mock):
"""
Given: A get_data response containing an XDR BIOC issue whose alert_description is a
structured list of indicator render-link tokens.
When: get_issues_by_filter_command is called.
Then: The issue's alert_description is rendered to plain text in BOTH the context
output and the human-readable table.
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = {
"reply": {
"DATA": [
{
"internal_id": 1,
"source_insert_ts": 1541494441222,
"alert_source": "BIOC",
"alert_description": [
{"render_type": "attribute", "pretty_name": "Action File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "evil.exe"},
],
}
],
"FILTER_COUNT": "1",
}
}
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
response = get_issues_by_filter_command(client, {})
assert response[0].outputs[0]["alert_description"] == "Action File Name = evil.exe"
assert "Action File Name = evil.exe" in response[0].readable_output
def test_get_issues_by_filter_does_not_render_non_bioc(requests_mock):
"""
Given: A get_data response containing a NON-BIOC issue.
When: get_issues_by_filter_command is called.
Then: The issue's alert_description is left untouched (even if it were a list).
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
description_list = [
{"render_type": "attribute", "pretty_name": "Action File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "evil.exe"},
]
api_response = {
"reply": {
"DATA": [
{
"internal_id": 2,
"source_insert_ts": 1541494441222,
"alert_source": "XDR Analytics",
"alert_description": json.loads(json.dumps(description_list)),
}
],
"FILTER_COUNT": "1",
}
}
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
response = get_issues_by_filter_command(client, {})
assert response[0].outputs[0]["alert_description"] == description_list
def test_get_issues_by_filter_xql_bioc_string_untouched(requests_mock):
"""
Given: An XQL BIOC issue whose alert_description is already a plain string.
When: get_issues_by_filter_command is called.
Then: The string description is left unchanged (only lists are rendered).
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
api_response = {
"reply": {
"DATA": [
{
"internal_id": 3,
"source_insert_ts": 1541494441222,
"alert_source": "BIOC",
"alert_description": "dataset = xdr_data | filter ...",
}
],
"FILTER_COUNT": "1",
}
}
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
response = get_issues_by_filter_command(client, {})
assert response[0].outputs[0]["alert_description"] == "dataset = xdr_data | filter ..."
def test_render_bioc_description_empty_list():
"""
Given: An empty indicator token list.
When: render_bioc_description is called.
Then: An empty string is returned (no crash).
"""
from CoreIRApiModule import render_bioc_description
assert render_bioc_description([]) == ""
def test_render_bioc_description_introduction_trailing_dash_trimmed():
"""
Given: An introduction token whose pretty_name ends with a dash, followed by a clause.
When: render_bioc_description is called.
Then: The introduction is included with its trailing dash trimmed.
"""
from CoreIRApiModule import render_bioc_description
indicator = [
{"render_type": "introduction", "pretty_name": "Suspicious activity -"},
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "evil.exe"},
]
result = render_bioc_description(indicator)
assert "Suspicious activity" in result
assert "Suspicious activity -" not in result # trailing dash removed
def test_render_bioc_description_comma_connector_spacing():
"""
Given: Values joined by a ',' connector (an OR-list of values).
When: render_bioc_description is called.
Then: The comma connector uses "<value>, <value>" spacing (no leading space before comma).
"""
from CoreIRApiModule import render_bioc_description
indicator = [
{"render_type": "value", "pretty_name": "a.exe"},
{"render_type": "connector", "pretty_name": ","},
{"render_type": "value", "pretty_name": "b.exe"},
]
assert render_bioc_description(indicator) == "a.exe, b.exe"
def test_get_issues_by_filter_bioc_description_render_failure_falls_back_to_simple(requests_mock, mocker):
"""
Given: A BIOC issue with a structured alert_description, where the full
render_bioc_description raises an exception.
When: get_issues_by_filter_command is called.
Then: The command does not crash, falls back to render_bioc_description_simple
(a plain pretty_name join), and demisto.error is called.
"""
from CoreIRApiModule import CoreClient, get_issues_by_filter_command
# Force the full renderer to fail so the simple fallback is exercised.
mocker.patch("CoreIRApiModule.render_bioc_description", side_effect=Exception("boom"))
description = [
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "evil.exe"},
]
api_response = {
"reply": {
"DATA": [
{
"internal_id": 4,
"source_insert_ts": 1541494441222,
"alert_source": "BIOC",
"alert_description": json.loads(json.dumps(description)),
}
],
"FILTER_COUNT": "1",
}
}
requests_mock.post(f"{Core_URL}/api/webapp/get_data/", json=api_response)
client = CoreClient(base_url=f"{Core_URL}/api/webapp", headers={})
response = get_issues_by_filter_command(client, {})
# Fell back to the simple pretty_name join.
assert response[0].outputs[0]["alert_description"] == "File Name = evil.exe"
def test_render_bioc_description_simple_joins_pretty_names():
"""
Given: A structured BIOC indicator token list.
When: render_bioc_description_simple is called.
Then: The pretty_name values are joined with single spaces.
"""
from CoreIRApiModule import render_bioc_description_simple
indicator = [
{"render_type": "attribute", "pretty_name": "File Name"},
{"render_type": "operator", "pretty_name": "="},
{"render_type": "value", "pretty_name": "evil.exe"},
]
assert render_bioc_description_simple(indicator) == "File Name = evil.exe"