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"],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
}