forked from p15670423/monkey
Merge pull request #1525 from guardicore/fix-t1075-reporting
Modify ATT&CK report messages for unscanned techniques
This commit is contained in:
commit
3133ee3217
|
@ -17,6 +17,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Resetting login credentials also cleans the contents of the database. #1495
|
- Resetting login credentials also cleans the contents of the database. #1495
|
||||||
- ATT&CK report messages (more accurate now). #1483
|
- ATT&CK report messages (more accurate now). #1483
|
||||||
- T1086 (PowerShell) now also reports if ps1 scripts were run by PBAs. #1513
|
- 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
|
### Removed
|
||||||
- Internet access check on agent start. #1402
|
- Internet access check on agent start. #1402
|
||||||
|
|
|
@ -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.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 import SCHEMA
|
||||||
from monkey_island.cc.services.config_schema.config_schema_per_attack_technique import (
|
from monkey_island.cc.services.config_schema.config_schema_per_attack_technique import (
|
||||||
get_config_schema_per_attack_technique,
|
ConfigSchemaPerAttackTechnique,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -122,8 +122,8 @@ class AttackTechnique(object, metaclass=abc.ABCMeta):
|
||||||
return disabled_msg
|
return disabled_msg
|
||||||
if status == ScanStatus.UNSCANNED.value:
|
if status == ScanStatus.UNSCANNED.value:
|
||||||
if not cls.config_schema_per_attack_technique:
|
if not cls.config_schema_per_attack_technique:
|
||||||
cls.config_schema_per_attack_technique = get_config_schema_per_attack_technique(
|
cls.config_schema_per_attack_technique = (
|
||||||
SCHEMA
|
ConfigSchemaPerAttackTechnique().get_config_schema_per_attack_technique(SCHEMA)
|
||||||
)
|
)
|
||||||
unscanned_msg = cls._get_unscanned_msg_with_reasons(
|
unscanned_msg = cls._get_unscanned_msg_with_reasons(
|
||||||
cls.unscanned_msg, cls.config_schema_per_attack_technique
|
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.")
|
reasons.append(f"- Monkey did not run on any {cls.relevant_systems[0]} systems.")
|
||||||
if cls.tech_id in config_schema_per_attack_technique:
|
if cls.tech_id in config_schema_per_attack_technique:
|
||||||
reasons.append(
|
reasons.append(
|
||||||
"- The following configuration options were disabled:<br/>"
|
"- The following configuration options were disabled or empty:<br/>"
|
||||||
f"{cls._get_relevant_config_values(config_schema_per_attack_technique)}"
|
f"{cls._get_relevant_config_values(config_schema_per_attack_technique)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
|
|
||||||
def get_config_schema_per_attack_technique(schema: Dict) -> Dict[str, Dict[str, List[str]]]:
|
class ConfigSchemaPerAttackTechnique:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.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 -
|
:return: dictionary mapping each attack technique to relevant config fields; example -
|
||||||
{
|
{
|
||||||
|
@ -13,24 +19,63 @@ def get_config_schema_per_attack_technique(schema: Dict) -> Dict[str, Dict[str,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
reverse_schema = {}
|
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"]
|
definitions = schema["definitions"]
|
||||||
for definition in definitions:
|
for definition in definitions:
|
||||||
definition_type = definitions[definition]["title"]
|
definition_type = definitions[definition]["title"]
|
||||||
for field in definitions[definition]["anyOf"]:
|
for field in definitions[definition].get("anyOf", []):
|
||||||
config_field = field["title"]
|
config_field = field["title"]
|
||||||
for attack_technique in field.get("attack_techniques", []):
|
for attack_technique in field.get("attack_techniques", []):
|
||||||
_add_config_field_to_reverse_schema(
|
self._add_config_field_to_reverse_schema(
|
||||||
definition_type, config_field, attack_technique, reverse_schema
|
definition_type, config_field, attack_technique
|
||||||
)
|
)
|
||||||
|
|
||||||
return reverse_schema
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
def _add_config_field_to_reverse_schema(
|
# check for "properties" and each property's related techniques recursively;
|
||||||
definition_type: str, config_field: str, attack_technique: str, reverse_schema: Dict
|
# the levels of nesting and where related techniques are declared won't
|
||||||
) -> None:
|
# always be fixed in the config schema
|
||||||
reverse_schema.setdefault(attack_technique, {})
|
self._crawl_properties(config_option_path, new_config_option)
|
||||||
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)
|
||||||
|
|
|
@ -340,6 +340,7 @@ INTERNAL = {
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"default": [],
|
"default": [],
|
||||||
"description": "List of LM hashes to use on exploits using credentials",
|
"description": "List of LM hashes to use on exploits using credentials",
|
||||||
|
"related_attack_techniques": ["T1075"],
|
||||||
},
|
},
|
||||||
"exploit_ntlm_hash_list": {
|
"exploit_ntlm_hash_list": {
|
||||||
"title": "Exploit NTLM hash list",
|
"title": "Exploit NTLM hash list",
|
||||||
|
@ -348,6 +349,7 @@ INTERNAL = {
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"default": [],
|
"default": [],
|
||||||
"description": "List of NTLM hashes to use on exploits using credentials",
|
"description": "List of NTLM hashes to use on exploits using credentials",
|
||||||
|
"related_attack_techniques": ["T1075"],
|
||||||
},
|
},
|
||||||
"exploit_ssh_keys": {
|
"exploit_ssh_keys": {
|
||||||
"title": "SSH key pairs list",
|
"title": "SSH key pairs list",
|
||||||
|
|
|
@ -8,28 +8,17 @@ from monkey_island.cc.services.attack.technique_reports.__init__ import (
|
||||||
disabled_msg,
|
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)
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
def mock_config_schema_per_attack_technique(monkeypatch, fake_schema):
|
def mock_config_schema_per_attack_technique(monkeypatch, fake_schema):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
("monkey_island.cc.services.attack.technique_reports." "__init__.SCHEMA"),
|
("monkey_island.cc.services.attack.technique_reports.__init__.SCHEMA"),
|
||||||
fake_schema,
|
fake_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeAttackTechnique_TwoRelevantSystems(AttackTechnique):
|
class FakeAttackTechnique_TwoRelevantSystems(AttackTechnique):
|
||||||
tech_id = "T0001"
|
tech_id = "T0000"
|
||||||
relevant_systems = ["System 1", "System 2"]
|
relevant_systems = ["System 1", "System 2"]
|
||||||
unscanned_msg = "UNSCANNED"
|
unscanned_msg = "UNSCANNED"
|
||||||
scanned_msg = "SCANNED"
|
scanned_msg = "SCANNED"
|
||||||
|
@ -42,9 +31,15 @@ class FakeAttackTechnique_TwoRelevantSystems(AttackTechnique):
|
||||||
class ExpectedMsgs_TwoRelevantSystems(Enum):
|
class ExpectedMsgs_TwoRelevantSystems(Enum):
|
||||||
UNSCANNED: str = (
|
UNSCANNED: str = (
|
||||||
"UNSCANNED due to one of the following reasons:\n"
|
"UNSCANNED due to one of the following reasons:\n"
|
||||||
"- The following configuration options were disabled:<br/>"
|
"- The following configuration options were disabled or empty:<br/>"
|
||||||
"- Definition Type 1 — Config Option 1<br/>"
|
"- Definition Type 1 — Config Option 1, Config Option 2<br/>"
|
||||||
"- Definition Type 2 — Config Option 5<br/>"
|
"- Definition Type 2 — Config Option 5, Config Option 6<br/>"
|
||||||
|
"- Property Type 1 -> Category 1 — Config Option 1<br/>"
|
||||||
|
"- Property Type 2 -> Category 1 — Config Option 1<br/>"
|
||||||
|
"- Property Type 2 -> Category 2 -> Config Option 1 — Config Option 1.1<br/>"
|
||||||
|
"- Property Type 2 -> Category 2 -> Config Option 2 — Config Option 2.1<br/>"
|
||||||
|
"- Property Type 2 -> Category 2 -> Config Option 2 -> Config Option 2.1 — Config Option "
|
||||||
|
"2.1.1<br/>"
|
||||||
)
|
)
|
||||||
SCANNED: str = "SCANNED"
|
SCANNED: str = "SCANNED"
|
||||||
USED: str = "USED"
|
USED: str = "USED"
|
||||||
|
@ -65,7 +60,7 @@ class ExpectedMsgs_OneRelevantSystem(Enum):
|
||||||
UNSCANNED: str = (
|
UNSCANNED: str = (
|
||||||
"UNSCANNED due to one of the following reasons:\n"
|
"UNSCANNED due to one of the following reasons:\n"
|
||||||
"- Monkey did not run on any System 1 systems.\n"
|
"- Monkey did not run on any System 1 systems.\n"
|
||||||
"- The following configuration options were disabled:<br/>"
|
"- The following configuration options were disabled or empty:<br/>"
|
||||||
"- Definition Type 1 — Config Option 1<br/>"
|
"- Definition Type 1 — Config Option 1<br/>"
|
||||||
"- Definition Type 2 — Config Option 5<br/>"
|
"- Definition Type 2 — Config Option 5<br/>"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
from monkey_island.cc.services.config_schema.config_schema_per_attack_technique import (
|
from monkey_island.cc.services.config_schema.config_schema_per_attack_technique import (
|
||||||
get_config_schema_per_attack_technique,
|
ConfigSchemaPerAttackTechnique,
|
||||||
)
|
)
|
||||||
|
|
||||||
REVERSE_FAKE_SCHEMA = {
|
REVERSE_FAKE_SCHEMA = {
|
||||||
"T0000": {
|
"T0000": {
|
||||||
"Definition Type 1": ["Config Option 1", "Config Option 2"],
|
"Definition Type 1": ["Config Option 1", "Config Option 2"],
|
||||||
"Definition Type 2": ["Config Option 5", "Config Option 6"],
|
"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": {
|
"T0001": {"Definition Type 1": ["Config Option 1"], "Definition Type 2": ["Config Option 5"]},
|
||||||
"Definition Type 1": ["Config Option 1"],
|
|
||||||
"Definition Type 2": ["Config Option 5"],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_config_schema_per_attack_technique(monkeypatch, fake_schema):
|
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
|
||||||
|
)
|
||||||
|
|
|
@ -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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue