import json import pytest import requests import demistomock as demisto from pytest_mock import MockerFixture from CommonServerPython import * from AutofocusV2 import Client IP_ADDRESS = "127.0.0.1" FILE_RES_JSON = {"indicator": {"latestPanVerdicts": {"WF_SAMPLE": "MALWARE"}}} IP_RES_JSON = { "indicator": { "indicatorValue": IP_ADDRESS, "indicatorType": "IPV4_ADDRESS", "summaryGenerationTs": 1576077539906, "latestPanVerdicts": {"PAN_DB": "MALWARE"}, "seenByDataSourceIds": ["WF_SAMPLE"], "wildfireRelatedSampleVerdictCounts": {"MALWARE": 95, "GRAYWARE": 5}, }, "tags": [ { "support_id": 1, "tag_name": "Upatre", "public_tag_name": "Unit42.Upatre", "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "count": 7692947, "lasthit": "2019-12-11 02:29:45", "description": "The Upatre Trojan typically arrives as an e-mail attachment or through" " an e-mail with a link to the malware. Upatre's function is to download additional" " malware onto the system, in most cases when the malware was initially observed" " downloading the Dyre banking Trojan which then attempts to steal the users online" " banking credentials which may may be used for fraud.\n\nSince then, the operators have" " diversified, with Upatre frequently seen downloading other banking trojans.\n", # noqa: E501 "customer_name": "Palo Alto Networks Unit42", "customer_industry": "High Tech", "upVotes": 2, "downVotes": None, "myVote": None, "source": "Unit 42", "tag_class_id": 3, "tag_definition_id": 29449, "tagGroups": [ { "tag_group_name": "Downloader", "description": "This type of malware secretly downloads malicious files from a remote server, " "then installs and executes the files.", # noqa: E501 } ], "aliases": ["Upatre"], } ], } INDICATOR_RES = { "IndicatorValue": IP_ADDRESS, "IndicatorType": "IPV4_ADDRESS", "LatestPanVerdicts": {"PAN_DB": "MALWARE"}, "WildfireRelatedSampleVerdictCounts": {"MALWARE": 95, "GRAYWARE": 5}, "SeenBy": ["WF_SAMPLE"], "Tags": [ { "PublicTagName": "Unit42.Upatre", "TagName": "Upatre", "CustomerName": "Palo Alto Networks Unit42", "Source": "Unit 42", "TagDefinitionScopeID": 4, "TagDefinitionStatusID": 1, "TagClassID": 3, "Count": 7692947, "Lasthit": "2019-12-11 02:29:45", "Description": "The Upatre Trojan typically arrives as an e-mail attachment or through an e-mail with a" " link to the malware. Upatre's function is to download additional malware onto the system," " in most cases when the malware was initially observed downloading the Dyre banking Trojan" " which then attempts to steal the users online banking credentials which may may be used " "for fraud.\n\nSince then, the operators have diversified, with Upatre frequently" " seen downloading other banking trojans.\n", # noqa: E501 } ], } TAGS_DETAILS_RES = { "public_tag_name": "Anon015b57.MYNEWTAGNAME", "tag_name": "MYNEWTAGNAME", "customer_name": "", "source": "Palo Alto Networks - InfoSec-Synack Tesing

xxx

", "tag_definition_scope": "public", "tag_definition_status": "disabled", "tag_class": "actor", "count": 108737, "lasthit": "2021-02-11 22:31:51", "description": "

xxx

", } RAW_TAGS = [ { "support_id": 1, "tag_name": "Upatre1", "public_tag_name": "Unit42.Upatre", "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "count": 7692947, "lasthit": "2019-12-11 02:29:45", "source": "Unit 42", "tag_class_id": 1, "tag_definition_id": 29449, }, { "support_id": 1, "tag_name": "Upatre2", "public_tag_name": "Unit42.Upatre", "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "count": 7692947, "lasthit": "2019-12-11 02:29:45", "source": "Unit 42", "tag_class_id": 2, "tag_definition_id": 29449, }, { "support_id": 1, "tag_name": "Upatre3", "public_tag_name": "Unit42.Upatre", "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "count": 7692947, "lasthit": "2019-12-11 02:29:45", "source": "Unit 42", "tag_class_id": 3, "tag_definition_id": 29449, }, { "support_id": 1, "tag_name": "Upatre5", "public_tag_name": "Unit42.Upatre", "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "count": 7692947, "lasthit": "2019-12-11 02:29:45", "source": "Unit 42", "tag_class_id": 5, "tag_definition_id": 29449, }, ] TAGS_FROM_FILE_RES = [ { "PublicTagName": "Commodity.Sivis", "TagName": "Sivis", "CustomerName": "Palo Alto Networks Unit42", "Source": None, "TagDefinitionScopeID": 3, "TagDefinitionStatusID": 1, "TagClassID": 3, "Count": 11778017, "TagGroups": {"TagGroupName": "GROUP"}, "Aliases": "ALIASES", "Lasthit": "2021-02-14 23:56:40", "Description": "A file infector which attempts to enumerate files on the host." " Modifies boot.ini and other system files to maintain persistence" " and spread.", } ] TAGS_FOR_GENERIC_CONTEXT_OUTPUT = [ { "PublicTagName": "Commodity.Sivis", "TagName": "Sivis", "TagGroups": {"TagGroupName": "GROUP"}, "Aliases": "ALIASES", } ] @pytest.fixture(autouse=True) def init_tests(mocker): params = {"api_key": "1234"} mocker.patch.object(demisto, "params", return_value=params) @pytest.fixture def autofocusv2_client(): return Client(url="url", verify=False) def util_load_json(path): with open(path) as f: return json.loads(f.read()) class ResMocker: def __init__(self, http_response, status_code): self.http_response = http_response self.status_code = status_code self.ok = False self.headers = {} def json(self): return self.http_response def raise_for_status(self): return def test_parse_indicator_response(): from AutofocusV2 import parse_indicator_response raw_indicator = IP_RES_JSON["indicator"] raw_tags = IP_RES_JSON["tags"] indicator = parse_indicator_response(raw_indicator, raw_tags, "IP") assert json.dumps(indicator) == json.dumps(INDICATOR_RES) def test_calculate_dbot_score(): from AutofocusV2 import calculate_dbot_score raw_indicator = IP_RES_JSON["indicator"] score = calculate_dbot_score(raw_indicator, "IP") assert score == 3 def test_calculate_dbot_score_file(): from AutofocusV2 import calculate_dbot_score raw_indicator = IP_RES_JSON["indicator"] score = calculate_dbot_score(raw_indicator, "File") assert score == 3 def test_connection_error(mocker, autofocusv2_client): import AutofocusV2 def raise_connection_error(url_suffix, method, ok_codes, headers, params, data={}, err_operation=None): assert url_suffix == "/tic" assert method == "GET" assert headers == {"Content-Type": "application/json", "apiKey": "1234"} assert params == { "indicatorType": "ip", "indicatorValue": "8.8.8.8", "includeTags": "true", } assert ok_codes == (200, 404, 409, 503) raise requests.exceptions.ConnectionError mocker.patch.object(autofocusv2_client, "http_request", side_effect=raise_connection_error) with pytest.raises( AutofocusV2.DemistoException, match="Error connecting to server. Check your URL/Proxy/Certificate settings" ): AutofocusV2.search_indicator(autofocusv2_client, "ip", "8.8.8.8") def test_tag_details(mocker, autofocusv2_client): """ Given: - The response from calling the command tag_details. When: - When the user uses 'autofocus-tag-details' for a given tag. Then: - The fields are being parsed properly in to context. """ import AutofocusV2 mocker.patch.object(demisto, "args", return_value={"tag_name": "Anon015b57.MYNEWTAGNAME"}) mocker.patch.object(AutofocusV2, "autofocus_tag_details", return_value=TAGS_DETAILS_RES) mocker.patch.object(demisto, "results") AutofocusV2.tag_details_command(autofocusv2_client) assert demisto.results.call_args[0][0] == util_load_json("test_data/teg_details_command_outputs.json") def test_get_tags_for_generic_context(): """ Given: - The 'Tags' values returned from the API for a given file. When: - When the user uses 'file' command. Then: - Only specific keys should be parsed in to context - 'TagGroups.TagGroupName', 'Aliases', 'PublicTagName', 'TagName'. """ import AutofocusV2 assert AutofocusV2.get_tags_for_generic_context(TAGS_FROM_FILE_RES) == TAGS_FOR_GENERIC_CONTEXT_OUTPUT def test_reliability(mocker, autofocusv2_client): import AutofocusV2 import CommonServerPython from CommonServerPython import DBotScoreReliability mock_data = { "indicator": { "indicatorValue": "1.1.1.1", "indicatorType": "IPV4_ADDRESS", "summaryGenerationTs": 1616340557369, "firstSeenTsGlobal": None, "lastSeenTsGlobal": None, "latestPanVerdicts": {"Test": "test"}, "seenByDataSourceIds": [], "wildfireRelatedSampleVerdictCounts": {}, }, "tags": [], } mocker.patch.object(AutofocusV2, "search_indicator", return_value=mock_data) mocked_dbot = mocker.patch.object(CommonServerPython.Common, "DBotScore") mocker.patch.object(CommonServerPython.Common, "IP") AutofocusV2.search_ip_command(autofocusv2_client, "1.1.1.1", DBotScoreReliability.B, False) assert mocked_dbot.call_args[1].get("reliability") == "B - Usually reliable" def test_get_tags_for_tags_and_malware_family_fields(): """ Given: - The 'Tags' values returned from the API for a given response. When: - When the user uses 'file' 'ip' 'domain' or 'url' commands. Then: - Only specific tags should be parsed in to context. """ import AutofocusV2 tags = AutofocusV2.get_tags_for_tags_and_malware_family_fields(TAGS_FROM_RESPONSE) tags.sort() assert tags == ["Bladabindi", "NJRat", "NanoCoreRAT", "RemoteAccessTrojan", "Unit42.NJRat", "Unit42.NanoCoreRAT"] TAGS_FROM_RESPONSE = [ { "aliases": ["Bladabindi"], "count": 2273664, "customer_industry": "High Tech", "customer_name": "Palo Alto Networks Unit42", "description": "NJRa.", "downVotes": "", "lasthit": "2020-11-17 12:04:36", "myVote": "", "public_tag_name": "Unit42.NJRat", "source": "Unit 42", "support_id": 1, "tagGroups": [{"description": "Remote", "tag_group_name": "RemoteAccessTrojan"}], "tag_class_id": 3, "tag_definition_id": 31426, "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "tag_name": "NJRat", "upVotes": 1, }, { "count": 506972, "customer_industry": "High Tech", "customer_name": "Palo Alto Networks Unit42", "description": "Generally", "downVotes": "", "lasthit": "2020-11-17 16:31:52", "myVote": "", "public_tag_name": "Unit42.NanoCoreRAT", "source": "Unit 42", "support_id": 1, "tag_class_id": 3, "tag_definition_id": 31987, "tag_definition_scope_id": 4, "tag_definition_status_id": 1, "tag_name": "NanoCoreRAT", "tagGroups": [{"description": "Remote", "tag_group_name": "RemoteAccessTrojan"}], "upVotes": 3, }, ] def test_create_relationships_list(): """ Given: - A tags list of the existing kinds of tags from the api. When: - When running create relationships. Then: - The relationships that are created contain the expected types and names. """ from AutofocusV2 import create_relationships_list expected_entity_b_types = ["Threat Actor", "Campaign", "Malware", "Attack Pattern"] expected_name = "indicator-of" expected_name_entity_b = ["Upatre1", "Upatre2", "Upatre3", "Upatre5"] relationships = create_relationships_list( entity_a="Test", entity_a_type="IP", tags=RAW_TAGS, reliability="B - Usually reliable" ) relation_entry = [relation.to_entry() for relation in relationships] for i, relation in enumerate(relation_entry): assert relation.get("name") == expected_name assert relation.get("entityA") == "Test" assert relation.get("entityB") == expected_name_entity_b[i] assert relation.get("entityBType") == expected_entity_b_types[i] URLS_LIST = [ ("www.München.com", "www.Mxn--tdanchen.com"), ("www.example.com", "www.example.com"), ("www.Mününchen.com", "www.Mxn--tdanxn--tdanchen.com"), ("www.Müünchen.com", "www.Mxn--tdaanchen.com"), ("www.MükÖnchen.com", "www.Mxn--tdakxn--ndanchen.com"), ("www.こんにちは.com", "www.xn--28j2a3ar1p.com"), ("https://paloaltonetworks–test.com", "https://paloaltonetworksxn--7ugtest.com"), # noqa: RUF001 ] @pytest.mark.parametrize("url, result", URLS_LIST) def test_convert_url_to_ascii_character(url, result): from AutofocusV2 import convert_url_to_ascii_character converted = convert_url_to_ascii_character(url) assert converted == result def test_search_url_command(mocker, autofocusv2_client): from AutofocusV2 import search_url_command mock_response = { "indicator": {"indicatorValue": "www.こんにちは.com", "indicatorType": "URL", "latestPanVerdicts": {"PAN_DB": "BENIGN"}}, "tags": [], } status_code = 200 response = ResMocker(mock_response, status_code) mocker.patch.object(autofocusv2_client, "http_request", return_value=response) result = search_url_command(autofocusv2_client, "www.こんにちは.com", "B - Usually reliable", True) assert result[0].indicator.url == "www.こんにちは.com" assert result[0].raw_response["indicator"]["indicatorValue"] == mock_response["indicator"]["indicatorValue"] @pytest.mark.parametrize( "separator, expected_value", [("|", ["https:firstpart,connectedpart"]), (None, ["https:firstpart", "connectedpart"])] ) def test_search_url_custom_separator(mocker, autofocusv2_client, separator, expected_value): from AutofocusV2 import search_url_command from CommonServerPython import remove_empty_elements # Mock response mock_response = {"indicator": {}, "tags": []} status_code = 200 response = ResMocker(mock_response, status_code) mocker.patch.object(autofocusv2_client, "http_request", return_value=response) # Mock argToList return_value = [] def side_effect_function(*args, **kwargs): result = argToList(*args, **kwargs) # Call the original function return_value.append(result) return result mocker.patch("AutofocusV2.argToList", side_effect=side_effect_function) # Mock args args = { "url": "https:firstpart,connectedpart", "reliability": "B - Usually reliable", "create_relationships": False, "separator": separator, } args: dict = remove_empty_elements(args) # type: ignore search_url_command(autofocusv2_client, **args) assert return_value[0] == expected_value def test_search_url_command_args(mocker, autofocusv2_client): from AutofocusV2 import search_url_command mock_response = { "indicator": {"indicatorValue": "www.test.com", "indicatorType": "URL", "latestPanVerdicts": {"PAN_DB": "BENIGN"}}, "tags": [], } status_code = 200 expected_headers = {"Content-Type": "application/json", "apiKey": "1234"} expected_params = {"indicatorType": "url", "indicatorValue": "www.test.com", "includeTags": "true"} expected_ok_codes = (200, 404, 409, 503) response = ResMocker(mock_response, status_code) http_request = mocker.patch.object(autofocusv2_client, "_http_request", return_value=response) search_url_command(autofocusv2_client, "www.test.com", "B - Usually reliable", True) http_request.assert_called_with( method="GET", url_suffix="/tic", data=json.dumps({}), headers=expected_headers, params=expected_params, retries=3, resp_type="response", ok_codes=expected_ok_codes, ) TEST_DATA = [ ("autofocus_md5_response", "123456789012345678901234567890ab", ["123456789012345678901234567890ab", None, None]), ( "autofocus_sha1_response", "1234567890123456789012345678901234567890", [None, "1234567890123456789012345678901234567890", None], ), ( "autofocus_sha256_response", "123456789012345678901234567890123456789012345678901234567890abcd", [None, None, "123456789012345678901234567890123456789012345678901234567890abcd"], ), ( "autofocus_sha256_response_wf_sample_has_null", "6833e945695d2609de175c5f67693594748229962759f366b822be5fd568f292", [None, None, "6833e945695d2609de175c5f67693594748229962759f366b822be5fd568f292"], ), ] @pytest.mark.parametrize("mock_response, file_hash, expected_results", TEST_DATA) def test_search_file_command(mocker, mock_response, file_hash, expected_results, autofocusv2_client): """ Given: - A file hash (md5, sha1, sha256). When: - When running search_file_command. Then: - Ensure the indicator contains the correct hash type. """ from AutofocusV2 import search_file_command with open(f"test_data/{mock_response}.json") as f: response_json = json.load(f) status_code = 200 response = ResMocker(response_json, status_code) mocker.patch.object(autofocusv2_client, "http_request", return_value=response) results = search_file_command(autofocusv2_client, file_hash, None, False) assert results[0].indicator.md5 == expected_results[0] assert results[0].indicator.sha1 == expected_results[1] assert results[0].indicator.sha256 == expected_results[2] assert results[0].outputs.get("IndicatorValue") in expected_results @pytest.mark.parametrize(argnames="mock_response, domain", argvalues=[("autofocus_domain_response", "mail16.amadeus.net")]) def test_search_domain_command(mock_response, domain, mocker, autofocusv2_client): """ Given: - A domain. When: - When running search_domain_command. Then: - Ensure the indicator contains the correct domain. """ from AutofocusV2 import search_domain_command with open(f"test_data/{mock_response}.json") as f: response_json = json.load(f) status_code = 200 response = ResMocker(response_json, status_code) mocker.patch.object(autofocusv2_client, "http_request", return_value=response) results = search_domain_command(autofocusv2_client, domain, None, False) assert results[0].indicator.domain == domain @pytest.mark.parametrize( argnames="ioc_type, ioc_val", argvalues=[ ("domain", "test_domain"), ("ipv4_address", "test_ipv4_address"), ("filehash", "123456789012345678901234567890ab"), ], ) def test_search_indicator_command__no_indicator(mocker, autofocusv2_client, ioc_type, ioc_val): """ Given: - Indicator not exist in AutoFocus When: - Run the reputation command Then: - Validate the expected result is return with detailed information """ # prepare from AutofocusV2 import search_ip_command, search_domain_command, search_file_command ioc_type_to_command = { "domain": search_domain_command, "ipv4_address": search_ip_command, "filehash": search_file_command, } no_indicator_response = {"tags": []} status_code = 200 response = ResMocker(no_indicator_response, status_code) mocker.patch.object(autofocusv2_client, "http_request", return_value=response) # run result = ioc_type_to_command[ioc_type](autofocusv2_client, ioc_val, "B - Usually reliable", True) # validate assert f"{ioc_val} was not found in AutoFocus" in result[0].readable_output def test_search_url_command__no_indicator(mocker, autofocusv2_client): """ Given: - url not exist in AutoFocus When: - Run the reputation command Then: - Validate the expected result is return with detailed information """ # prepare from AutofocusV2 import search_url_command no_indicator_response = {"tags": []} status_code = 200 response = ResMocker(no_indicator_response, status_code) mocker.patch.object(autofocusv2_client, "http_request", return_value=response) # run result = search_url_command(autofocusv2_client, "test_url", "B - Usually reliable", True) # validate assert "test_url was not found in AutoFocus" in result[0].readable_output @pytest.mark.parametrize("range_num,res_count", [(98, 1), (250, 3)]) def test_search_session(mocker, autofocusv2_client, range_num, res_count): """ Given: - Large amount of IPs to search sessions on. When: - Running the search_session. Then: - Validate the search is done for each batch of 100 IPs and the cookies are returned accordingly. """ from AutofocusV2 import search_sessions status_code = 200 response = ResMocker({"af_cookie": "auto-focus-cookie"}, status_code) mocker.patch.object(autofocusv2_client, "_http_request", return_value=response) ips = [f"{i}.{i}.{i}.{i}" for i in range(range_num)] res = search_sessions(client=autofocusv2_client, ip=ips, use_batches=True) assert len(res) == res_count for r in res: assert r.get("AFCookie") == "auto-focus-cookie" @pytest.mark.parametrize("range_num", [98, 250]) def test_search_session_no_batches(mocker, autofocusv2_client, range_num): """ Given: - Large amount of IPs to search sessions on. When: - Running the search_session with use_batches=False. Then: - Validate the search is done without batching and returns a single result. """ from AutofocusV2 import search_sessions status_code = 200 response = ResMocker({"af_cookie": "auto-focus-cookie"}, status_code) mocker.patch.object(autofocusv2_client, "_http_request", return_value=response) ips = [f"{i}.{i}.{i}.{i}" for i in range(range_num)] res = search_sessions(client=autofocusv2_client, ip=ips, use_batches=False) assert isinstance(res, dict) assert res.get("AFCookie") == "auto-focus-cookie" @pytest.mark.parametrize("range_num,res_count", [(98, 1), (250, 3)]) def test_search_samples(mocker, autofocusv2_client, range_num, res_count): """ Given: - Large amount of IPs to search samples on. When: - Running the search_samples. Then: - Validate the search is done for each batch of 100 IPs and the cookies are returned accordingly. """ from AutofocusV2 import search_samples status_code = 200 response = ResMocker({"af_cookie": "auto-focus-cookie"}, status_code) mocker.patch.object(autofocusv2_client, "_http_request", return_value=response) ips = [f"{i}.{i}.{i}.{i}" for i in range(range_num)] res = search_samples(client=autofocusv2_client, ip=ips, use_batches=True) assert len(res) == res_count for r in res: assert r.get("AFCookie") == "auto-focus-cookie" @pytest.mark.parametrize("range_num", [98, 250]) def test_search_samples_no_batches(mocker, autofocusv2_client, range_num): """ Given: - Large amount of IPs to search samples on. When: - Running the search_samples with use_batches=False. Then: - Validate the search is done without batching and returns a single result. """ from AutofocusV2 import search_samples status_code = 200 response = ResMocker({"af_cookie": "auto-focus-cookie"}, status_code) mocker.patch.object(autofocusv2_client, "_http_request", return_value=response) ips = [f"{i}.{i}.{i}.{i}" for i in range(range_num)] res = search_samples(client=autofocusv2_client, ip=ips, use_batches=False) assert isinstance(res, dict) assert res.get("AFCookie") == "auto-focus-cookie" def test_metrics(mocker: MockerFixture, autofocusv2_client): import AutofocusV2 bucket_info = { "bucket_info": { "minute_points": 200, "daily_points": 30000, "minute_points_remaining": 0, "daily_points_remaining": 4578, "minute_bucket_start": "2015-09-02 10:55:33", "daily_bucket_start": "2015-09-01 17:08:40", "wait_in_seconds": 20.5, } } mocker.patch.object(demisto, "command", return_value="autofocus-top-tags-search") mocker.patch.object(demisto, "args", return_value={"unit42": "True", "class": "Actor", "retry_on_rate_limit": "true"}) mocker.patch.object(demisto, "demistoVersion", return_value={"version": "6.9.0", "buildNumber": "12345"}) response = ResMocker(bucket_info, 503) mocker.patch.object(autofocusv2_client, "_http_request", return_value=response) mocker.patch("AutofocusV2.Client", return_value=autofocusv2_client) return_results_mock = mocker.patch("AutofocusV2.return_results") AutofocusV2.EXECUTION_METRICS = ExecutionMetrics() AutofocusV2.main() autofocusv2_client._http_request.assert_called_with( method="POST", headers={"Content-Type": "application/json"}, data=json.dumps( { "query": {"operator": "all", "children": [{"field": "sample.tag_class", "operator": "is", "value": "actor"}]}, "scope": None, "tagScopes": ["unit42"], "apiKey": "1234", } ), url_suffix="/top-tags/search/", ok_codes=(200, 409, 503), resp_type="response", retries=3, params={}, ) assert return_results_mock.call_args_list[0][0][0].readable_output == "API Rate limit exceeded, rerunning command." assert return_results_mock.call_args_list[0][0][0].scheduled_command._args == { "unit42": "True", "class": "Actor", "retry_on_rate_limit": "false", } assert return_results_mock.call_args_list[0][0][0].scheduled_command._next_run == "40" assert return_results_mock.call_args_list[1][0][0].execution_metrics == [{"APICallsCount": 1, "Type": "QuotaError"}] assert ( return_results_mock.call_args_list[2][0][0].readable_output == """### Autofocus API Points |Daily allotment started|Daily points used|Minute allotment started|Minute points used| |---|---|---|---| | 2015-09-01 17:08:40 | 4578/30000 | 2015-09-02 10:55:33 | 0/200 | """ )