import json import demistomock as demisto # noqa: F401 import pytest from stix2 import TAXIICollectionSource, parse from test_data.mitre_test_data import ( ATTACK_PATTERN, ATTACK_PATTERNS, CAMPAIGN, COURSE_OF_ACTION, ID_TO_NAME, INDICATORS_LIST, INTRUSION_SET, MALWARE, MALWARE_LIST_WITH_PREFIX, MALWARE_LIST_WITHOUT_PREFIX, MITRE_ID_TO_MITRE_NAME, NEW_ID_TO_NAME, NEW_INDICATORS_LIST, OLD_ID_TO_NAME, RELATION, RELATIONSHIP_ENTITY, TOOL, ) ENTERPRISE_COLLECTION_ID = " x-mitre-collection–1f5f1533-f617-4ca8-9ab4-6a02367fa019" # noqa: RUF001 NON_ENTERPRISE_COLLECTION_ID = "101010101010101010101010101010101" class MockCollection: def __init__(self, id_, title): self.id = id_ self.title = title def mock_create_relations(original): def mock(item_json, id_to_name): return original(item_json, ID_TO_NAME) return mock @pytest.mark.parametrize( "indicator, expected_result", [ ([ATTACK_PATTERN.get("response")], ATTACK_PATTERN.get("indicator")), ([COURSE_OF_ACTION.get("response")], COURSE_OF_ACTION.get("indicator")), ([INTRUSION_SET.get("response")], INTRUSION_SET.get("indicator")), ([MALWARE.get("response")], MALWARE.get("indicator")), ([TOOL.get("response")], TOOL.get("indicator")), ], ) def test_fetch_indicators(mocker, indicator, expected_result): """ Given - fetch incidents command - command args - command raw response When - mock the Client's get_stix_objects. Then - run the fetch incidents command using the Client Validate that all the indicators extracted successfully """ import FeedMitreAttackv2 as fm from FeedMitreAttackv2 import Client, create_relationship client = Client(url="https://test.org", proxies=False, verify=False, tags=[], tlp_color=None) client.tactic_name_to_mitre_id = { "Defense Evasion": "TA0005", "Privilege Escalation": "TA0004", "Resource Development": "TA0042", } default_id = ENTERPRISE_COLLECTION_ID nondefault_id = 2 client.collections = [MockCollection(default_id, "enterprise att&ck"), MockCollection(nondefault_id, "not_default")] mocker.patch.object(client, "initialise") mocker.patch.object(TAXIICollectionSource, "__init__", return_value=None) mocker.patch.object(TAXIICollectionSource, "query", return_value=indicator) # mocker.patch.object(json, 'loads', return_value=indicator[0]) mocker.patch.object(fm, "create_relationship", wraps=mock_create_relations(create_relationship)) indicators = client.build_iterator(create_relationships=True, limit=7) assert indicators == expected_result default_id = NON_ENTERPRISE_COLLECTION_ID nondefault_id = 2 client.collections = [MockCollection(default_id, "default"), MockCollection(nondefault_id, "not_default")] mocker.patch.object(client, "initialise") mocker.patch.object(TAXIICollectionSource, "__init__", return_value=None) mocker.patch.object(TAXIICollectionSource, "query", return_value=indicator) mocker.patch.object(json, "loads", return_value=indicator[0]) mocker.patch.object(fm, "create_relationship", wraps=mock_create_relations(create_relationship)) indicators = client.build_iterator(create_relationships=True, limit=7) assert indicators == ([], [], {}, {}) @pytest.mark.parametrize( "field_name, field_value, expected_result", [ ("created", "2017-05-31T21:31:43.540Z", "2017-05-31T21:31:43.540Z"), ("created", "2019-04-25T20:53:07.719Z\n2019-04-25T20:53:07.814Z", "2019-04-25T20:53:07.719Z"), ("modified", "2017-05-31T21:31:43.540Z", "2017-05-31T21:31:43.540Z"), ("modified", "2020-03-16T15:38:37.650Z\n2020-01-17T16:45:24.252Z", "2020-03-16T15:38:37.650Z"), ], ) def test_handle_multiple_dates_in_one_field(field_name, field_value, expected_result): """ Given - created / modified indicator field When - this field contains two dates Then - run the handle_multiple_dates_in_one_field Validate The field contain one specific date. """ from FeedMitreAttackv2 import handle_multiple_dates_in_one_field assert handle_multiple_dates_in_one_field(field_name, field_value) == expected_result @pytest.mark.parametrize( "indicator, expected_result", [({"x_mitre_deprecated": True}, True), ({"revoked": True}, True), ({}, False)] ) def test_is_indicator_deprecated_or_revoked(indicator, expected_result): """ Given - indicator in STIX format. When - we cheed Then - run the create_list_relationships Validate The relationships list extracted successfully. """ from FeedMitreAttackv2 import is_indicator_deprecated_or_revoked assert is_indicator_deprecated_or_revoked(indicator) == expected_result @pytest.mark.parametrize( "indicator_type, indicator_json, expected_result", [ ("Attack Pattern", ATTACK_PATTERN.get("response"), ATTACK_PATTERN.get("map_result")), ("Course of Action", COURSE_OF_ACTION.get("response"), COURSE_OF_ACTION.get("map_result")), ("Intrusion Set", INTRUSION_SET.get("response"), INTRUSION_SET.get("map_result")), ("Malware", MALWARE.get("response"), MALWARE.get("map_result")), ("Tool", TOOL.get("response"), TOOL.get("map_result")), ("Campaign", CAMPAIGN.get("response"), CAMPAIGN.get("map_result")), ], ) def test_map_fields_by_type(indicator_type, indicator_json, expected_result): from FeedMitreAttackv2 import map_fields_by_type assert map_fields_by_type(indicator_type, indicator_json) == expected_result def test_create_relationship(): """ Given - relationship obj in STIX format. When - we extract this relationship to Demisto format Then - run the create_relationship Validate The relationship extracted successfully. """ from FeedMitreAttackv2 import create_relationship relation = create_relationship(RELATION.get("response"), ID_TO_NAME) relation._entity_a = "entity a" relation._entity_a_type = "STIX Malware" relation._entity_b = "entity b" relation._entity_b_type = "STIX Attack Pattern" relation._name = "uses" relation._relation_type = "IndicatorToIndicator" relation._reverse_name = "used-by" def test_get_item_type(): from FeedMitreAttackv2 import get_item_type assert get_item_type("malware", True) == "Malware" assert get_item_type("malware", False) == "STIX Malware" assert get_item_type("intrusion-set", True) == "Intrusion Set" assert get_item_type("intrusion-set", False) == "Intrusion Set" def test_create_relationship_list(): from FeedMitreAttackv2 import create_relationship_list assert create_relationship_list([RELATION.get("response")], ID_TO_NAME) == RELATION.get("indicator") def test_add_malware_prefix_to_dup_with_intrusion_set(): from FeedMitreAttackv2 import add_malware_prefix_to_dup_with_intrusion_set malware_list = MALWARE_LIST_WITHOUT_PREFIX add_malware_prefix_to_dup_with_intrusion_set(MALWARE_LIST_WITHOUT_PREFIX, ID_TO_NAME) assert malware_list == MALWARE_LIST_WITH_PREFIX def test_add_obj_to_mitre_id_to_mitre_name(): from FeedMitreAttackv2 import add_obj_to_mitre_id_to_mitre_name mitre_id_to_mitre_name = {} add_obj_to_mitre_id_to_mitre_name(mitre_id_to_mitre_name, ATTACK_PATTERN["response"]) assert mitre_id_to_mitre_name == {"T1047": "ATTACK_PATTERN 1"} def test_add_technique_prefix_to_sub_technique(): from FeedMitreAttackv2 import add_technique_prefix_to_sub_technique indicators = INDICATORS_LIST mitre_id_to_mitre_name = MITRE_ID_TO_MITRE_NAME id_to_name = OLD_ID_TO_NAME add_technique_prefix_to_sub_technique(indicators, id_to_name, mitre_id_to_mitre_name) assert indicators == NEW_INDICATORS_LIST assert id_to_name == NEW_ID_TO_NAME def test_publication_link_not_none(): from FeedMitreAttackv2 import map_fields_by_type indicator = {"created": "2022-01-05T14:27:46.612705Z", "modified": "2022-01-05T14:27:46.612705Z", "external_references": [{}]} res = map_fields_by_type("Malware", indicator) assert res["publications"][0]["link"] is not None def test_create_relationships_invalid(): from FeedMitreAttackv2 import create_relationship item_json = {"source_ref": "", "target_ref": ""} assert create_relationship(item_json, {}) is None def test_create_relationship_with_unknown_relationship_name(): from FeedMitreAttackv2 import create_relationship item_json = {"source_ref--source_ref": "source_ref", "target_ref--target_ref": "target_ref"} output = create_relationship(RELATIONSHIP_ENTITY, item_json) assert output is not None @pytest.mark.parametrize( "attack_id, attack_pattern_obj, expected_result", [ ("T1111", {"external_references": [{"external_id": "T1111"}]}, True), ("T1098", {"external_references": [{"external_id": "T1111"}]}, False), ], ) def test_filter_attack_pattern_object_by_attack_id(attack_id, attack_pattern_obj, expected_result): from FeedMitreAttackv2 import filter_attack_pattern_object_by_attack_id output = filter_attack_pattern_object_by_attack_id(attack_id, attack_pattern_obj) assert output == expected_result @pytest.mark.parametrize( "description, expected_result", [ ("Test (23)", ""), ("Test (2020, Mar)", "2020-03-01T00:00:00"), ("Test (Test) (2020, Mar)", "2020-03-01T00:00:00"), ("Test 2033)", ""), ("Test ()", ""), ("Test (Test)", ""), ("Gross, J. (2016, February 23). Operation Dust Storm. Retrieved December 22, 2021.", "2016-02-23T00:00:00"), ("Cisco. (n.d.). Cisco IOS Software Integrity Assurance - Command History. Retrieved October 21, 2020.", ""), ("Citation: Security Affairs Elderwood Sept 2012)", ""), ("Insikt Group (Recorded Future). (2017, May 17).", "2017-05-17T00:00:00"), ("Insikt Group (Recorded Future). (2017, May17).", "2017-05-17T00:00:00"), ("Insikt Group (Recorded Future). (2017,May17).", "2017-05-17T00:00:00"), ("Insikt Group (Recorded Future). (2017,March17).", "2017-03-17T00:00:00"), ("Insikt Group (Recorded Future). (2017, March 17).", "2017-03-17T00:00:00"), ], ) def test_extract_date_time_from_description(description, expected_result): from FeedMitreAttackv2 import extract_date_time_from_description output = extract_date_time_from_description(description) assert output == expected_result def test_attack_pattern_reputation_command(mocker): """ Given: Some attack patterns to retrieve, with and without sub-technique When: Running attack-pattern reputation command Then: Returns the wanted attack patterns """ from FeedMitreAttackv2 import attack_pattern_reputation_command stix_objs = [parse(stix_obj_dict, allow_custom=True) for stix_obj_dict in ATTACK_PATTERNS] mocker.patch("FeedMitreAttackv2.get_mitre_data_by_filter", return_value=stix_objs) args = {"attack_pattern": "Abuse Elevation Control Mechanism, Active Scanning: Wordlist Scanning"} command_results = attack_pattern_reputation_command("", args) assert command_results[0].indicator.value == "Abuse Elevation Control Mechanism" assert command_results[1].indicator.value == "Active Scanning: Wordlist Scanning" def test_attack_pattern_reputation_without_answer_command(mocker): """ Given: One attach pattern to retrive data on, that is not found in the collection When: Running attack-pattern reputation command Then: Ensures the command_results is not empty and readable_output is as expected """ from FeedMitreAttackv2 import attack_pattern_reputation_command stix_objs = [parse(stix_obj_dict, allow_custom=True) for stix_obj_dict in ATTACK_PATTERNS] mocker.patch("FeedMitreAttackv2.get_mitre_data_by_filter", return_value=stix_objs) args = {"attack_pattern": "dummy attack pattern"} command_results = attack_pattern_reputation_command("", args) assert command_results assert ( command_results.readable_output == "MITRE ATTACK Attack Patterns values: No Attack " "Patterns found for ['dummy attack pattern'] in the Enterprise collection." ) def test_get_mitre_value_from_id_without_answer_command(mocker): """ Given: One attach pattern to retrive data on, that is not found in the collection When: Running attack-pattern reputation command Then: Ensures the command_results is not empty and readable_output is as expected """ from FeedMitreAttackv2 import get_mitre_value_from_id stix_objs = [parse(stix_obj_dict, allow_custom=True) for stix_obj_dict in ATTACK_PATTERNS] mocker.patch("FeedMitreAttackv2.get_mitre_data_by_filter", return_value=stix_objs) args = {"attack_ids": ["dummy attack pattern id"]} command_results = get_mitre_value_from_id("", args) assert command_results assert ( command_results.readable_output == "MITRE ATTACK Attack Patterns values: " "No Attack Patterns found for ['dummy attack pattern id'] in the " "Enterprise collection." ) @pytest.mark.parametrize( "description, expected_result", [ ( "Waterbear is modular malware attributed to BlackTech ...(Citation: Trend Micro Waterbear December 2019)", "Waterbear is modular malware attributed to BlackTech ...", ), ( "Adversaries may employ various means to detect and avoid debuggers.(Citation: ProcessHacker Github)\ (assuming a present debugger would “swallow” or handle the potential error).\ (Citation: hasherezade debug)(Citation: AlKhaser Debug)(Citation: vxunderground debug)\ OutputDebugStringW().(Citation: wardle evilquest partii)(Citation: Checkpoint Dridex Jan 2021)", "Adversaries may employ various means to detect and avoid debuggers.\ (assuming a present debugger would “swallow” or handle the potential error).\ OutputDebugStringW().", ), ], ) def test_remove_citations(description, expected_result): """ Given: A description with Citation. When: Calling remove_citation method. Then: Output description will not contain Citation parts. """ from FeedMitreAttackv2 import remove_citations actual_result = remove_citations(description) assert "Citation" not in actual_result assert actual_result == expected_result def test_create_indicator_with_unknown_tactic_name(mocker): """ Given: An Attack Pattern whose kill_chain_phases contains a tactic phase name ('stealth') that is NOT present in the client's tactic_name_to_mitre_id mapping. When: Calling client.create_indicator(). Then: The indicator is created successfully without raising a KeyError. Both relationships are created: the known tactic uses the full "ID - Name" format, and the unknown tactic falls back to using just its name. """ from FeedMitreAttackv2 import Client client = Client(url="https://test.org", proxies=False, verify=False, tags=[], tlp_color=None) # Only known tactics are pre-populated; 'Stealth' is intentionally absent client.tactic_name_to_mitre_id = { "Defense Evasion": "TA0005", } mitre_item_json = { "id": "attack-pattern--test-stealth-1234", "name": "Some Technique", "type": "attack-pattern", "created": "2024-01-01T00:00:00.000Z", "modified": "2024-01-01T00:00:00.000Z", "description": "A technique that uses stealth.", "external_references": [ {"source_name": "mitre-attack", "external_id": "T9999", "url": "https://attack.mitre.org/techniques/T9999"}, ], "kill_chain_phases": [ {"kill_chain_name": "mitre-attack", "phase_name": "defense-evasion"}, {"kill_chain_name": "mitre-attack", "phase_name": "stealth"}, # new unknown tactic ], "x_mitre_platforms": ["Windows"], } mocker.patch("FeedMitreAttackv2.demisto.debug") # Should not raise KeyError indicator = client.create_indicator("Attack Pattern", "Some Technique", mitre_item_json) assert indicator["value"] == "Some Technique" # Both tactics should appear in relationships: # - known tactic uses full "ID - Name" format # - unknown tactic falls back to name-only (no MITRE ID prefix) assert len(indicator["relationships"]) == 2 entity_b_values = {rel["entityB"] for rel in indicator["relationships"]} assert "TA0005 - Defense Evasion" in entity_b_values assert "Stealth" in entity_b_values def test_get_mitre_value_from_id_with_valid_ids(mocker): """ Given: Valid MITRE technique IDs (T1111 and T1111.001) that exist in the collection. When: Running mitre-get-indicator-name command. Then: Returns the correct attack pattern names, including parent: sub-technique format. """ from FeedMitreAttackv2 import get_mitre_value_from_id stix_objs = [parse(stix_obj_dict, allow_custom=True) for stix_obj_dict in ATTACK_PATTERNS] mocker.patch("FeedMitreAttackv2.get_mitre_data_by_filter", return_value=stix_objs) # T1111 is a top-level technique ("Active Scanning") args = {"attack_ids": ["T1111"]} command_results = get_mitre_value_from_id("", args) assert command_results assert command_results.outputs assert command_results.outputs[0]["id"] == "T1111" assert command_results.outputs[0]["value"] == "Active Scanning" def test_get_mitre_value_from_id_sub_technique_with_parent(mocker): """ Given: A valid MITRE sub-technique ID (T1111.001) whose parent (T1111) exists in the collection. When: Running mitre-get-indicator-name command. Then: Returns the attack pattern name in "Parent: Sub-technique" format. """ from FeedMitreAttackv2 import get_mitre_value_from_id stix_objs = [parse(stix_obj_dict, allow_custom=True) for stix_obj_dict in ATTACK_PATTERNS] mocker.patch("FeedMitreAttackv2.get_mitre_data_by_filter", return_value=stix_objs) # T1111.001 is "Wordlist Scanning", parent T1111 is "Active Scanning" args = {"attack_ids": ["T1111.001"]} command_results = get_mitre_value_from_id("", args) assert command_results assert command_results.outputs assert command_results.outputs[0]["id"] == "T1111.001" assert command_results.outputs[0]["value"] == "Active Scanning: Wordlist Scanning" def test_get_mitre_value_from_id_sub_technique_missing_parent(mocker): """ Given: A MITRE sub-technique ID whose parent technique is NOT in the collection. When: Running mitre-get-indicator-name command. Then: Does not raise an IndexError; returns the sub-technique name without parent prefix. """ from FeedMitreAttackv2 import get_mitre_value_from_id # Only include the sub-technique, not the parent sub_technique_only = [ATTACK_PATTERNS[0]] # T1111.001 "Wordlist Scanning", parent T1111 not included stix_objs = [parse(stix_obj_dict, allow_custom=True) for stix_obj_dict in sub_technique_only] mocker.patch("FeedMitreAttackv2.get_mitre_data_by_filter", return_value=stix_objs) args = {"attack_ids": ["T1111.001"]} # Should not raise IndexError command_results = get_mitre_value_from_id("", args) assert command_results assert command_results.outputs assert command_results.outputs[0]["id"] == "T1111.001" # Without parent, name is just the sub-technique name assert command_results.outputs[0]["value"] == "Wordlist Scanning" def test_show_feeds_command(mocker): """ Given: A Client. When: Calling show_feeds_command method. Then: Validate the output extracted successfully. """ from FeedMitreAttackv2 import Client, show_feeds_command client = Client(url="https://test.org", proxies=False, verify=False, tags=[], tlp_color=None) default_id = NON_ENTERPRISE_COLLECTION_ID nondefault_id = 2 client.collections = [MockCollection(default_id, "default"), MockCollection(nondefault_id, "not_default")] mocker.patch.object(demisto, "results") show_feeds_command(client) assert demisto.results.call_count == 1 assert demisto.results.call_args[0][0] == { "Type": 1, "Contents": [{"Name": "default", "ID": "101010101010101010101010101010101"}, {"Name": "not_default", "ID": 2}], "ContentsFormat": "json", "HumanReadable": "### MITRE ATT&CK Feeds:\n|Name|ID|\n|---|---|\n| default |\ 101010101010101010101010101010101 |\n| not_default | 2 |\n", "ReadableContentsFormat": "markdown", }