diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e358f3d5..d4ba8e014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Resetting login credentials also cleans the contents of the database. #1495 - ATT&CK report messages (more accurate now). #1483 - T1086 (PowerShell) now also reports if ps1 scripts were run by PBAs. #1513 +- ATT&CK report messages to include empty internal config options as reasons for unscanned attack + techniques. #1518 ### Removed - Internet access check on agent start. #1402 diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py index 30405dd69..529cf7b95 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -9,7 +9,7 @@ from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations from monkey_island.cc.services.attack.attack_config import AttackConfig from monkey_island.cc.services.config_schema.config_schema import SCHEMA from monkey_island.cc.services.config_schema.config_schema_per_attack_technique import ( - get_config_schema_per_attack_technique, + ConfigSchemaPerAttackTechnique, ) logger = logging.getLogger(__name__) @@ -122,8 +122,8 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): return disabled_msg if status == ScanStatus.UNSCANNED.value: if not cls.config_schema_per_attack_technique: - cls.config_schema_per_attack_technique = get_config_schema_per_attack_technique( - SCHEMA + cls.config_schema_per_attack_technique = ( + ConfigSchemaPerAttackTechnique().get_config_schema_per_attack_technique(SCHEMA) ) unscanned_msg = cls._get_unscanned_msg_with_reasons( cls.unscanned_msg, cls.config_schema_per_attack_technique @@ -143,7 +143,7 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): reasons.append(f"- Monkey did not run on any {cls.relevant_systems[0]} systems.") if cls.tech_id in config_schema_per_attack_technique: reasons.append( - "- The following configuration options were disabled:
" + "- The following configuration options were disabled or empty:
" f"{cls._get_relevant_config_values(config_schema_per_attack_technique)}" ) diff --git a/monkey/monkey_island/cc/services/config_schema/config_schema_per_attack_technique.py b/monkey/monkey_island/cc/services/config_schema/config_schema_per_attack_technique.py index 9b7cd6bb2..547161936 100644 --- a/monkey/monkey_island/cc/services/config_schema/config_schema_per_attack_technique.py +++ b/monkey/monkey_island/cc/services/config_schema/config_schema_per_attack_technique.py @@ -1,36 +1,81 @@ from typing import Dict, List -def get_config_schema_per_attack_technique(schema: Dict) -> Dict[str, Dict[str, List[str]]]: - """ - :return: dictionary mapping each attack technique to relevant config fields; example - - { - "T1003": { - "System Info Collectors": [ - "Mimikatz collector", - "Azure credential collector" - ] - } - } - """ - reverse_schema = {} +class ConfigSchemaPerAttackTechnique: + def __init__(self) -> None: + self.reverse_schema = {} - definitions = schema["definitions"] - for definition in definitions: - definition_type = definitions[definition]["title"] - for field in definitions[definition]["anyOf"]: - config_field = field["title"] - for attack_technique in field.get("attack_techniques", []): - _add_config_field_to_reverse_schema( - definition_type, config_field, attack_technique, reverse_schema + def get_config_schema_per_attack_technique( + self, schema: Dict + ) -> Dict[str, Dict[str, List[str]]]: + """ + :return: dictionary mapping each attack technique to relevant config fields; example - + { + "T1003": { + "System Info Collectors": [ + "Mimikatz collector", + "Azure credential collector" + ] + } + } + """ + self._crawl_config_schema_definitions_for_reverse_schema(schema) + self._crawl_config_schema_properties_for_reverse_schema(schema) + + return self.reverse_schema + + def _crawl_config_schema_definitions_for_reverse_schema(self, schema: Dict): + definitions = schema["definitions"] + for definition in definitions: + definition_type = definitions[definition]["title"] + for field in definitions[definition].get("anyOf", []): + config_field = field["title"] + for attack_technique in field.get("attack_techniques", []): + self._add_config_field_to_reverse_schema( + definition_type, config_field, attack_technique + ) + + def _crawl_config_schema_properties_for_reverse_schema(self, schema: Dict): + properties = schema["properties"] + for prop in properties: + property_type = properties[prop]["title"] + for category_name in properties[prop].get("properties", []): + category = properties[prop]["properties"][category_name] + self._crawl_properties( + config_option_path=property_type, + config_option=category, ) - return reverse_schema + def _crawl_properties(self, config_option_path: str, config_option: Dict): + config_option_path = ( + f"{config_option_path} -> {config_option['title']}" + if "title" in config_option + else config_option_path + ) + for config_option_name in config_option.get("properties", []): + new_config_option = config_option["properties"][config_option_name] + self._check_related_attack_techniques( + config_option_path=config_option_path, + config_option=new_config_option, + ) + # check for "properties" and each property's related techniques recursively; + # the levels of nesting and where related techniques are declared won't + # always be fixed in the config schema + self._crawl_properties(config_option_path, new_config_option) -def _add_config_field_to_reverse_schema( - definition_type: str, config_field: str, attack_technique: str, reverse_schema: Dict -) -> None: - reverse_schema.setdefault(attack_technique, {}) - reverse_schema[attack_technique].setdefault(definition_type, []) - reverse_schema[attack_technique][definition_type].append(config_field) + def _check_related_attack_techniques(self, config_option_path: str, config_option: Dict): + for attack_technique in config_option.get("related_attack_techniques", []): + # No config values could be a reason that related attack techniques are left + # unscanned. See https://github.com/guardicore/monkey/issues/1518 for more. + config_field = config_option["title"] + self._add_config_field_to_reverse_schema( + config_option_path, config_field, attack_technique + ) + + def _add_config_field_to_reverse_schema( + self, definition_type: str, config_field: str, attack_technique: str + ) -> None: + self.reverse_schema.setdefault(attack_technique, {}) + self.reverse_schema[attack_technique].setdefault(definition_type, []) + self.reverse_schema[attack_technique][definition_type].append(config_field) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index b6e926dfb..84baa6ca5 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -340,6 +340,7 @@ INTERNAL = { "items": {"type": "string"}, "default": [], "description": "List of LM hashes to use on exploits using credentials", + "related_attack_techniques": ["T1075"], }, "exploit_ntlm_hash_list": { "title": "Exploit NTLM hash list", @@ -348,6 +349,7 @@ INTERNAL = { "items": {"type": "string"}, "default": [], "description": "List of NTLM hashes to use on exploits using credentials", + "related_attack_techniques": ["T1075"], }, "exploit_ssh_keys": { "title": "SSH key pairs list", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/attack/technique_reports/test_technique_reports.py b/monkey/tests/unit_tests/monkey_island/cc/services/attack/technique_reports/test_technique_reports.py index 82a23a9ae..1da9860b9 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/attack/technique_reports/test_technique_reports.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/attack/technique_reports/test_technique_reports.py @@ -8,28 +8,17 @@ from monkey_island.cc.services.attack.technique_reports.__init__ import ( disabled_msg, ) -FAKE_CONFIG_SCHEMA_PER_ATTACK_TECHNIQUE = { - "T0000": { - "Definition Type 1": ["Config Option 1", "Config Option 2"], - "Definition Type 2": ["Config Option 5", "Config Option 6"], - }, - "T0001": { - "Definition Type 1": ["Config Option 1"], - "Definition Type 2": ["Config Option 5"], - }, -} - @pytest.fixture(scope="function", autouse=True) def mock_config_schema_per_attack_technique(monkeypatch, fake_schema): monkeypatch.setattr( - ("monkey_island.cc.services.attack.technique_reports." "__init__.SCHEMA"), + ("monkey_island.cc.services.attack.technique_reports.__init__.SCHEMA"), fake_schema, ) class FakeAttackTechnique_TwoRelevantSystems(AttackTechnique): - tech_id = "T0001" + tech_id = "T0000" relevant_systems = ["System 1", "System 2"] unscanned_msg = "UNSCANNED" scanned_msg = "SCANNED" @@ -42,9 +31,15 @@ class FakeAttackTechnique_TwoRelevantSystems(AttackTechnique): class ExpectedMsgs_TwoRelevantSystems(Enum): UNSCANNED: str = ( "UNSCANNED due to one of the following reasons:\n" - "- The following configuration options were disabled:
" - "- Definition Type 1 — Config Option 1
" - "- Definition Type 2 — Config Option 5
" + "- The following configuration options were disabled or empty:
" + "- Definition Type 1 — Config Option 1, Config Option 2
" + "- Definition Type 2 — Config Option 5, Config Option 6
" + "- Property Type 1 -> Category 1 — Config Option 1
" + "- Property Type 2 -> Category 1 — Config Option 1
" + "- Property Type 2 -> Category 2 -> Config Option 1 — Config Option 1.1
" + "- Property Type 2 -> Category 2 -> Config Option 2 — Config Option 2.1
" + "- Property Type 2 -> Category 2 -> Config Option 2 -> Config Option 2.1 — Config Option " + "2.1.1
" ) SCANNED: str = "SCANNED" USED: str = "USED" @@ -65,7 +60,7 @@ class ExpectedMsgs_OneRelevantSystem(Enum): UNSCANNED: str = ( "UNSCANNED due to one of the following reasons:\n" "- Monkey did not run on any System 1 systems.\n" - "- The following configuration options were disabled:
" + "- The following configuration options were disabled or empty:
" "- Definition Type 1 — Config Option 1
" "- Definition Type 2 — Config Option 5
" ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/config_schema/test_config_schema_per_attack_technique.py b/monkey/tests/unit_tests/monkey_island/cc/services/config_schema/test_config_schema_per_attack_technique.py index bacdae5dd..86383366e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/config_schema/test_config_schema_per_attack_technique.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/config_schema/test_config_schema_per_attack_technique.py @@ -1,18 +1,25 @@ from monkey_island.cc.services.config_schema.config_schema_per_attack_technique import ( - get_config_schema_per_attack_technique, + ConfigSchemaPerAttackTechnique, ) REVERSE_FAKE_SCHEMA = { "T0000": { "Definition Type 1": ["Config Option 1", "Config Option 2"], "Definition Type 2": ["Config Option 5", "Config Option 6"], + "Property Type 1 -> Category 1": ["Config Option 1"], + "Property Type 2 -> Category 1": ["Config Option 1"], + "Property Type 2 -> Category 2 -> Config Option 1": ["Config Option 1.1"], + "Property Type 2 -> Category 2 -> Config Option 2": ["Config Option 2.1"], + "Property Type 2 -> Category 2 -> Config Option 2 -> Config Option 2.1": [ + "Config Option 2.1.1" + ], }, - "T0001": { - "Definition Type 1": ["Config Option 1"], - "Definition Type 2": ["Config Option 5"], - }, + "T0001": {"Definition Type 1": ["Config Option 1"], "Definition Type 2": ["Config Option 5"]}, } def test_get_config_schema_per_attack_technique(monkeypatch, fake_schema): - assert get_config_schema_per_attack_technique(fake_schema) == REVERSE_FAKE_SCHEMA + assert ( + ConfigSchemaPerAttackTechnique().get_config_schema_per_attack_technique(fake_schema) + == REVERSE_FAKE_SCHEMA + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py index b89be55f9..bd0744f17 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -66,5 +66,64 @@ def fake_schema(): }, ], }, - } + }, + "properties": { + "property_type_1": { + "title": "Property Type 1", + "properties": { + "category_1": { + "title": "Category 1", + "properties": { + "config_option_1": { + "title": "Config Option 1", + "related_attack_techniques": ["T0000"], + }, + }, + }, + }, + }, + "property_type_2": { + "title": "Property Type 2", + "properties": { + "category_1": { + "title": "Category 1", + "properties": { + "config_option_1": { + "title": "Config Option 1", + "related_attack_techniques": ["T0000"], + }, + }, + }, + "category_2": { + "title": "Category 2", + "properties": { + "config_option_1": { + "title": "Config Option 1", + "properties": { + "config_option_1.1": { + "title": "Config Option 1.1", + "related_attack_techniques": ["T0000"], + }, + }, + }, + "config_option_2": { + "title": "Config Option 2", + "properties": { + "config_option_2.1": { + "title": "Config Option 2.1", + "properties": { + "config_option_2.1.1": { + "title": "Config Option 2.1.1", + "related_attack_techniques": ["T0000"], + } + }, + "related_attack_techniques": ["T0000"], + }, + }, + }, + }, + }, + }, + }, + }, }