Merge pull request #2031 from guardicore/1960-configuration-repository

1960 configuration repository
This commit is contained in:
Mike Salvatore 2022-06-17 19:44:09 -04:00 committed by GitHub
commit 58733f7572
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 469 additions and 144 deletions

View File

@ -2,3 +2,4 @@ from .agent_configuration import (
AgentConfiguration,
AgentConfigurationSchema,
)
from .default_agent_configuration import DEFAULT_AGENT_CONFIGURATION

View File

@ -0,0 +1,217 @@
DEFAULT_AGENT_CONFIGURATION = """{
"keep_tunnel_open_time": 30,
"post_breach_actions": [
{
"name": "CommunicateAsBackdoorUser",
"options": {}
},
{
"name": "ModifyShellStartupFiles",
"options": {}
},
{
"name": "HiddenFiles",
"options": {}
},
{
"name": "TrapCommand",
"options": {}
},
{
"name": "ChangeSetuidSetgid",
"options": {}
},
{
"name": "ScheduleJobs",
"options": {}
},
{
"name": "Timestomping",
"options": {}
},
{
"name": "AccountDiscovery",
"options": {}
},
{
"name": "ProcessListCollection",
"options": {}
}
],
"credential_collectors": [
{
"name": "MimikatzCollector",
"options": {}
},
{
"name": "SSHCollector",
"options": {}
}
],
"payloads": [
{
"name": "ransomware",
"options": {
"encryption": {
"enabled": true,
"directories": {
"linux_target_dir": "",
"windows_target_dir": ""
}
},
"other_behaviors": {
"readme": true
}
}
}
],
"custom_pbas": {
"linux_command": "",
"linux_filename": "",
"windows_command": "",
"windows_filename": ""
},
"propagation": {
"maximum_depth": 2,
"network_scan": {
"tcp": {
"timeout": 3000,
"ports": [
22,
80,
135,
443,
445,
2222,
3306,
3389,
5985,
5986,
7001,
8008,
8080,
8088,
8983,
9200,
9600
]
},
"icmp": {
"timeout": 1000
},
"fingerprinters": [
{
"name": "elastic",
"options": {}
},
{
"name": "http",
"options": {
"http_ports": [
80,
443,
7001,
8008,
8080,
8983,
9200,
9600
]
}
},
{
"name": "mssql",
"options": {}
},
{
"name": "smb",
"options": {}
},
{
"name": "ssh",
"options": {}
}
],
"targets": {
"blocked_ips": [],
"inaccessible_subnets": [],
"local_network_scan": true,
"subnets": []
}
},
"exploitation": {
"options": {
"http_ports": [
80,
443,
7001,
8008,
8080,
8983,
9200,
9600
]
},
"brute_force": [
{
"name": "MSSQLExploiter",
"options": {},
"supported_os": [
"WINDOWS"
]
},
{
"name": "PowerShellExploiter",
"options": {},
"supported_os": [
"WINDOWS"
]
},
{
"name": "SSHExploiter",
"options": {},
"supported_os": [
"LINUX"
]
},
{
"name": "SmbExploiter",
"options": {
"smb_download_timeout": 30
},
"supported_os": [
"WINDOWS"
]
},
{
"name": "WmiExploiter",
"options": {
"smb_download_timeout": 30
},
"supported_os": [
"WINDOWS"
]
}
],
"vulnerability": [
{
"name": "HadoopExploiter",
"options": {},
"supported_os": [
"LINUX",
"WINDOWS"
]
},
{
"name": "Log4ShellExploiter",
"options": {},
"supported_os": [
"LINUX",
"WINDOWS"
]
}
]
}
}
}
"""

View File

@ -1,3 +1,6 @@
from .errors import RetrievalError
from .file_storage import FileRetrievalError, IFileRepository, LocalStorageFileRepository
from .i_agent_binary_repository import IAgentBinaryRepository, AgentRetrievalError
from .agent_binary_repository import AgentBinaryRepository
from .i_agent_configuration_repository import IAgentConfigurationRepository
from .file_agent_configuration_repository import FileAgentConfigurationRepository

View File

@ -0,0 +1,2 @@
class RetrievalError(RuntimeError):
pass

View File

@ -0,0 +1,38 @@
import io
from common.configuration import (
DEFAULT_AGENT_CONFIGURATION,
AgentConfiguration,
AgentConfigurationSchema,
)
from monkey_island.cc.repository import (
IAgentConfigurationRepository,
IFileRepository,
RetrievalError,
)
AGENT_CONFIGURATION_FILE_NAME = "agent_configuration.json"
class FileAgentConfigurationRepository(IAgentConfigurationRepository):
def __init__(self, file_repository: IFileRepository):
self._file_repository = file_repository
self._schema = AgentConfigurationSchema()
def get_configuration(self) -> AgentConfiguration:
try:
with self._file_repository.open_file(AGENT_CONFIGURATION_FILE_NAME) as f:
configuration_json = f.read().decode()
return self._schema.loads(configuration_json)
# TODO: Handle FileRetrievalError vs FileNotFoundError
# https://github.com/guardicore/monkey/blob/e8001d8cf76340e42bf17ff62523bd2d85fc4841/monkey/monkey_island/cc/repository/file_storage/local_storage_file_repository.py#L47-L50
except RetrievalError:
return self._schema.loads(DEFAULT_AGENT_CONFIGURATION)
def store_configuration(self, agent_configuration: AgentConfiguration):
configuration_json = self._schema.dumps(agent_configuration)
self._file_repository.save_file(
AGENT_CONFIGURATION_FILE_NAME, io.BytesIO(configuration_json.encode())
)

View File

@ -1,8 +1,10 @@
import abc
from typing import BinaryIO
from monkey_island.cc.repository import RetrievalError
class FileRetrievalError(RuntimeError):
class FileRetrievalError(RetrievalError):
pass

View File

@ -0,0 +1,29 @@
from abc import ABC, abstractmethod
from common.configuration import AgentConfiguration
class IAgentConfigurationRepository(ABC):
"""
A repository used to store and retrieve the agent configuration.
"""
@abstractmethod
def get_configuration(self) -> AgentConfiguration:
"""
Retrieve the agent configuration from the repository
:return: The agent configuration as retrieved from the repository, or the default
configuration if none could be retrieved.
:raises RetrievalError: if the configuration can not be retrieved
"""
pass
@abstractmethod
def store_configuration(self, agent_configuration: AgentConfiguration):
"""
Store the agent configuration in the repository
:param agent_configuration: The agent configuration to store in the repository
"""
pass

View File

@ -1,30 +0,0 @@
from abc import ABC
from typing import Any, Mapping, Sequence
class IConfigRepository(ABC):
# Config
###############################################
# This returns the current config
# TODO investigate if encryption should be here or where
# TODO potentially should be a DTO as well, but it's structure is defined in schema already
def get_config(self) -> Mapping:
pass
def set_config(self, config: dict):
pass
# Used when only a subset of config is submitted, for example only PBAFiles
# Used by passing keys, like ['monkey', 'post_breach_actions', 'linux_filename']
# Using a list is less ambiguous IMO, than using . notation
def set_config_field(self, key_list: Sequence[str], value: Any):
pass
# Used when only a subset of config is needed, for example only PBAFiles
# Used by passing keys, like ['monkey', 'post_breach_actions', 'linux_filename']
# Using a list is less ambiguous IMO, than using . notation
# TODO Still in doubt about encryption, this should probably be determined automatically
def get_config_field(self, key_list: Sequence[str]) -> Any:
pass

View File

@ -7,7 +7,9 @@ from common.utils.file_utils import get_binary_io_sha256_hash
from monkey_island.cc.repository import (
AgentBinaryRepository,
AgentRetrievalError,
FileAgentConfigurationRepository,
IAgentBinaryRepository,
IAgentConfigurationRepository,
IFileRepository,
LocalStorageFileRepository,
)
@ -31,11 +33,14 @@ def initialize_services(data_dir: Path) -> DIContainer:
container.register_instance(AWSInstance, AWSInstance())
container.register_instance(
IFileRepository, LocalStorageFileRepository(data_dir / "custom_pbas")
IFileRepository, LocalStorageFileRepository(data_dir / "runtime_data")
)
container.register_instance(AWSService, container.resolve(AWSService))
container.register_instance(IAgentBinaryRepository, _build_agent_binary_repository())
container.register_instance(LocalMonkeyRunService, container.resolve(LocalMonkeyRunService))
container.register_instance(
IAgentConfigurationRepository, container.resolve(FileAgentConfigurationRepository)
)
# This is temporary until we get DI all worked out.
PostBreachFilesService.initialize(container.resolve(IFileRepository))

View File

@ -0,0 +1,75 @@
PLUGIN_NAME = "bond"
PLUGIN_OPTIONS = {"gun": "Walther PPK", "car": "Aston Martin DB5"}
PLUGIN_CONFIGURATION = {"name": PLUGIN_NAME, "options": PLUGIN_OPTIONS}
LINUX_COMMAND = "a"
LINUX_FILENAME = "b"
WINDOWS_COMMAND = "c"
WINDOWS_FILENAME = "d"
CUSTOM_PBA_CONFIGURATION = {
"linux_command": LINUX_COMMAND,
"linux_filename": LINUX_FILENAME,
"windows_command": WINDOWS_COMMAND,
"windows_filename": WINDOWS_FILENAME,
}
BLOCKED_IPS = ["10.0.0.1", "192.168.1.1"]
INACCESSIBLE_SUBNETS = ["172.0.0.0/24", "172.2.2.0/24", "192.168.56.0/24"]
LOCAL_NETWORK_SCAN = True
SUBNETS = ["10.0.0.2", "10.0.0.2/16"]
SCAN_TARGET_CONFIGURATION = {
"blocked_ips": BLOCKED_IPS,
"inaccessible_subnets": INACCESSIBLE_SUBNETS,
"local_network_scan": LOCAL_NETWORK_SCAN,
"subnets": SUBNETS,
}
TIMEOUT = 2.525
ICMP_CONFIGURATION = {"timeout": TIMEOUT}
PORTS = [8080, 443]
TCP_SCAN_CONFIGURATION = {"timeout": TIMEOUT, "ports": PORTS}
FINGERPRINTERS = [{"name": "mssql", "options": {}}]
NETWORK_SCAN_CONFIGURATION = {
"tcp": TCP_SCAN_CONFIGURATION,
"icmp": ICMP_CONFIGURATION,
"fingerprinters": FINGERPRINTERS,
"targets": SCAN_TARGET_CONFIGURATION,
}
BRUTE_FORCE = [
{"name": "ex1", "options": {}, "supported_os": ["LINUX"]},
{
"name": "ex2",
"options": {"smb_download_timeout": 10},
"supported_os": ["LINUX", "WINDOWS"],
},
]
VULNERABILITY = [
{
"name": "ex3",
"options": {"smb_download_timeout": 10},
"supported_os": ["WINDOWS"],
},
]
EXPLOITATION_CONFIGURATION = {
"options": {"http_ports": PORTS},
"brute_force": BRUTE_FORCE,
"vulnerability": VULNERABILITY,
}
PROPAGATION_CONFIGURATION = {
"maximum_depth": 5,
"network_scan": NETWORK_SCAN_CONFIGURATION,
"exploitation": EXPLOITATION_CONFIGURATION,
}
AGENT_CONFIGURATION = {
"keep_tunnel_open_time": 30,
"custom_pbas": CUSTOM_PBA_CONFIGURATION,
"post_breach_actions": [PLUGIN_CONFIGURATION],
"credential_collectors": [PLUGIN_CONFIGURATION],
"payloads": [PLUGIN_CONFIGURATION],
"propagation": PROPAGATION_CONFIGURATION,
}

View File

@ -0,0 +1 @@
from .single_file_repository import SingleFileRepository

View File

@ -0,0 +1,23 @@
import io
from typing import BinaryIO
from monkey_island.cc.repository import FileRetrievalError, IFileRepository
class SingleFileRepository(IFileRepository):
def __init__(self):
self._file = None
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
self._file = io.BytesIO(file_contents.read())
def open_file(self, unsafe_file_name: str) -> BinaryIO:
if self._file is None:
raise FileRetrievalError()
return self._file
def delete_file(self, unsafe_file_name: str):
self._file = None
def delete_all_files(self):
self.delete_file("")

View File

@ -1,5 +1,34 @@
from tests.common.example_agent_configuration import (
AGENT_CONFIGURATION,
BLOCKED_IPS,
CUSTOM_PBA_CONFIGURATION,
EXPLOITATION_CONFIGURATION,
FINGERPRINTERS,
ICMP_CONFIGURATION,
INACCESSIBLE_SUBNETS,
LINUX_COMMAND,
LINUX_FILENAME,
LOCAL_NETWORK_SCAN,
NETWORK_SCAN_CONFIGURATION,
PLUGIN_CONFIGURATION,
PLUGIN_NAME,
PLUGIN_OPTIONS,
PORTS,
PROPAGATION_CONFIGURATION,
SCAN_TARGET_CONFIGURATION,
SUBNETS,
TCP_SCAN_CONFIGURATION,
TIMEOUT,
WINDOWS_COMMAND,
WINDOWS_FILENAME,
)
from common import OperatingSystems
from common.configuration import AgentConfiguration, AgentConfigurationSchema
from common.configuration import (
DEFAULT_AGENT_CONFIGURATION,
AgentConfiguration,
AgentConfigurationSchema,
)
from common.configuration.agent_sub_configuration_schemas import (
CustomPBAConfigurationSchema,
ExploitationConfigurationSchema,
@ -20,30 +49,14 @@ from common.configuration.agent_sub_configurations import (
PropagationConfiguration,
)
NAME = "bond"
OPTIONS = {"gun": "Walther PPK", "car": "Aston Martin DB5"}
PLUGIN_CONFIGURATION = {"name": NAME, "options": OPTIONS}
def test_build_plugin_configuration():
schema = PluginConfigurationSchema()
config = schema.load(PLUGIN_CONFIGURATION)
assert config.name == NAME
assert config.options == OPTIONS
LINUX_COMMAND = "a"
LINUX_FILENAME = "b"
WINDOWS_COMMAND = "c"
WINDOWS_FILENAME = "d"
CUSTOM_PBA_CONFIGURATION = {
"linux_command": LINUX_COMMAND,
"linux_filename": LINUX_FILENAME,
"windows_command": WINDOWS_COMMAND,
"windows_filename": WINDOWS_FILENAME,
}
assert config.name == PLUGIN_NAME
assert config.options == PLUGIN_OPTIONS
def test_custom_pba_configuration_schema():
@ -57,18 +70,6 @@ def test_custom_pba_configuration_schema():
assert config.windows_filename == WINDOWS_FILENAME
BLOCKED_IPS = ["10.0.0.1", "192.168.1.1"]
INACCESSIBLE_SUBNETS = ["172.0.0.0/24", "172.2.2.0/24", "192.168.56.0/24"]
LOCAL_NETWORK_SCAN = True
SUBNETS = ["10.0.0.2", "10.0.0.2/16"]
SCAN_TARGET_CONFIGURATION = {
"blocked_ips": BLOCKED_IPS,
"inaccessible_subnets": INACCESSIBLE_SUBNETS,
"local_network_scan": LOCAL_NETWORK_SCAN,
"subnets": SUBNETS,
}
def test_scan_target_configuration():
schema = ScanTargetConfigurationSchema()
@ -80,10 +81,6 @@ def test_scan_target_configuration():
assert config.subnets == SUBNETS
TIMEOUT = 2.525
ICMP_CONFIGURATION = {"timeout": TIMEOUT}
def test_icmp_scan_configuration_schema():
schema = ICMPScanConfigurationSchema()
@ -92,11 +89,6 @@ def test_icmp_scan_configuration_schema():
assert config.timeout == TIMEOUT
PORTS = [8080, 443]
TCP_SCAN_CONFIGURATION = {"timeout": TIMEOUT, "ports": PORTS}
def test_tcp_scan_configuration_schema():
schema = TCPScanConfigurationSchema()
@ -106,15 +98,6 @@ def test_tcp_scan_configuration_schema():
assert config.ports == PORTS
FINGERPRINTERS = [{"name": "mssql", "options": {}}]
NETWORK_SCAN_CONFIGURATION = {
"tcp": TCP_SCAN_CONFIGURATION,
"icmp": ICMP_CONFIGURATION,
"fingerprinters": FINGERPRINTERS,
"targets": SCAN_TARGET_CONFIGURATION,
}
def test_network_scan_configuration():
schema = NetworkScanConfigurationSchema()
@ -155,28 +138,6 @@ def test_exploiter_configuration_schema():
assert config.supported_os == supported_os
BRUTE_FORCE = [
{"name": "ex1", "options": {}, "supported_os": ["LINUX"]},
{
"name": "ex2",
"options": {"smb_download_timeout": 10},
"supported_os": ["LINUX", "WINDOWS"],
},
]
VULNERABILITY = [
{
"name": "ex3",
"options": {"smb_download_timeout": 10},
"supported_os": ["WINDOWS"],
},
]
EXPLOITATION_CONFIGURATION = {
"options": {"http_ports": PORTS},
"brute_force": BRUTE_FORCE,
"vulnerability": VULNERABILITY,
}
def test_exploitation_configuration():
schema = ExploitationConfigurationSchema()
@ -187,13 +148,6 @@ def test_exploitation_configuration():
assert config_dict == EXPLOITATION_CONFIGURATION
PROPAGATION_CONFIGURATION = {
"maximum_depth": 5,
"network_scan": NETWORK_SCAN_CONFIGURATION,
"exploitation": EXPLOITATION_CONFIGURATION,
}
def test_propagation_configuration():
schema = PropagationConfigurationSchema()
@ -208,17 +162,9 @@ def test_propagation_configuration():
def test_agent_configuration():
agent_configuration = {
"keep_tunnel_open_time": 30,
"custom_pbas": CUSTOM_PBA_CONFIGURATION,
"post_breach_actions": [PLUGIN_CONFIGURATION],
"credential_collectors": [PLUGIN_CONFIGURATION],
"payloads": [PLUGIN_CONFIGURATION],
"propagation": PROPAGATION_CONFIGURATION,
}
schema = AgentConfigurationSchema()
config = schema.load(agent_configuration)
config = schema.load(AGENT_CONFIGURATION)
config_dict = schema.dump(config)
assert isinstance(config, AgentConfiguration)
@ -228,4 +174,12 @@ def test_agent_configuration():
assert isinstance(config.credential_collectors[0], PluginConfiguration)
assert isinstance(config.payloads[0], PluginConfiguration)
assert isinstance(config.propagation, PropagationConfiguration)
assert config_dict == agent_configuration
assert config_dict == AGENT_CONFIGURATION
def test_default_agent_configuration():
schema = AgentConfigurationSchema()
config = schema.loads(DEFAULT_AGENT_CONFIGURATION)
assert isinstance(config, AgentConfiguration)

View File

@ -0,0 +1,26 @@
from tests.common.example_agent_configuration import AGENT_CONFIGURATION
from tests.monkey_island import SingleFileRepository
from common.configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfigurationSchema
from monkey_island.cc.repository import FileAgentConfigurationRepository
def test_store_agent_config():
repository = FileAgentConfigurationRepository(SingleFileRepository())
schema = AgentConfigurationSchema()
agent_configuration = schema.load(AGENT_CONFIGURATION)
repository.store_configuration(agent_configuration)
retrieved_agent_configuration = repository.get_configuration()
assert retrieved_agent_configuration == agent_configuration
def test_get_default_agent_config():
repository = FileAgentConfigurationRepository(SingleFileRepository())
schema = AgentConfigurationSchema()
default_agent_configuration = schema.loads(DEFAULT_AGENT_CONFIGURATION)
retrieved_agent_configuration = repository.get_configuration()
assert retrieved_agent_configuration == default_agent_configuration

View File

@ -1,12 +1,10 @@
import io
from typing import BinaryIO
import pytest
from tests.common import StubDIContainer
from tests.monkey_island import SingleFileRepository
from tests.unit_tests.monkey_island.conftest import get_url_for_resource
from tests.utils import raise_
from monkey_island.cc.repository import FileRetrievalError, IFileRepository
from monkey_island.cc.repository import IFileRepository
from monkey_island.cc.resources.pba_file_upload import LINUX_PBA_TYPE, WINDOWS_PBA_TYPE, FileUpload
TEST_FILE_CONTENTS = b"m0nk3y"
@ -40,28 +38,9 @@ def mock_get_config_value(monkeypatch):
)
class MockFileRepository(IFileRepository):
def __init__(self):
self._file = None
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
self._file = io.BytesIO(file_contents.read())
def open_file(self, unsafe_file_name: str) -> BinaryIO:
if self._file is None:
raise FileRetrievalError()
return self._file
def delete_file(self, unsafe_file_name: str):
self._file = None
def delete_all_files(self):
self.delete_file("")
@pytest.fixture
def file_repository():
return MockFileRepository()
return SingleFileRepository()
@pytest.fixture