Merge pull request #2045 from guardicore/1960-configuration-object

1960 configuration object
This commit is contained in:
Mike Salvatore 2022-06-27 10:58:41 -04:00 committed by GitHub
commit 5a0d891c35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 1918 additions and 853 deletions

View File

@ -2,3 +2,4 @@
Used for a common things between agent and island
"""
from .di_container import DIContainer, UnregisteredTypeError
from .operating_systems import OperatingSystems

View File

@ -0,0 +1,15 @@
from .agent_configuration import AgentConfiguration, InvalidConfigurationError
from .agent_sub_configurations import (
CustomPBAConfiguration,
PluginConfiguration,
ScanTargetConfiguration,
ICMPScanConfiguration,
TCPScanConfiguration,
NetworkScanConfiguration,
ExploitationOptionsConfiguration,
ExploitationConfiguration,
PropagationConfiguration,
)
from .default_agent_configuration import (
DEFAULT_AGENT_CONFIGURATION,
)

View File

@ -0,0 +1,97 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, List, Mapping
from marshmallow import Schema, fields
from marshmallow.exceptions import MarshmallowError
from .agent_sub_configuration_schemas import (
CustomPBAConfigurationSchema,
PluginConfigurationSchema,
PropagationConfigurationSchema,
)
from .agent_sub_configurations import (
CustomPBAConfiguration,
PluginConfiguration,
PropagationConfiguration,
)
class InvalidConfigurationError(Exception):
pass
INVALID_CONFIGURATION_ERROR_MESSAGE = (
"Cannot construct an AgentConfiguration object with the supplied, invalid data:"
)
@dataclass(frozen=True)
class AgentConfiguration:
keep_tunnel_open_time: float
custom_pbas: CustomPBAConfiguration
post_breach_actions: List[PluginConfiguration]
credential_collectors: List[PluginConfiguration]
payloads: List[PluginConfiguration]
propagation: PropagationConfiguration
def __post_init__(self):
# This will raise an exception if the object is invalid. Calling this in __post__init()
# makes it impossible to construct an invalid object
try:
AgentConfigurationSchema().dump(self)
except Exception as err:
raise InvalidConfigurationError(f"{INVALID_CONFIGURATION_ERROR_MESSAGE}: {err}")
@staticmethod
def from_mapping(config_mapping: Mapping[str, Any]) -> AgentConfiguration:
"""
Construct an AgentConfiguration from a Mapping
:param config_mapping: A Mapping that represents an AgentConfiguration
:return: An AgentConfiguration
:raises: InvalidConfigurationError if the provided Mapping does not represent a valid
AgentConfiguration
"""
try:
config_dict = AgentConfigurationSchema().load(config_mapping)
return AgentConfiguration(**config_dict)
except MarshmallowError as err:
raise InvalidConfigurationError(f"{INVALID_CONFIGURATION_ERROR_MESSAGE}: {err}")
@staticmethod
def from_json(config_json: str) -> AgentConfiguration:
"""
Construct an AgentConfiguration from a JSON string
:param config_json: A JSON string that represents an AgentConfiguration
:return: An AgentConfiguration
:raises: InvalidConfigurationError if the provided JSON does not represent a valid
AgentConfiguration
"""
try:
config_dict = AgentConfigurationSchema().loads(config_json)
return AgentConfiguration(**config_dict)
except MarshmallowError as err:
raise InvalidConfigurationError(f"{INVALID_CONFIGURATION_ERROR_MESSAGE}: {err}")
@staticmethod
def to_json(config: AgentConfiguration) -> str:
"""
Serialize an AgentConfiguration to JSON
:param config: An AgentConfiguration
:return: A JSON string representing the AgentConfiguration
"""
return AgentConfigurationSchema().dumps(config)
class AgentConfigurationSchema(Schema):
keep_tunnel_open_time = fields.Float()
custom_pbas = fields.Nested(CustomPBAConfigurationSchema)
post_breach_actions = fields.List(fields.Nested(PluginConfigurationSchema))
credential_collectors = fields.List(fields.Nested(PluginConfigurationSchema))
payloads = fields.List(fields.Nested(PluginConfigurationSchema))
propagation = fields.Nested(PropagationConfigurationSchema)

View File

@ -0,0 +1,100 @@
from marshmallow import Schema, fields, post_load
from .agent_sub_configurations import (
CustomPBAConfiguration,
ExploitationConfiguration,
ExploitationOptionsConfiguration,
ICMPScanConfiguration,
NetworkScanConfiguration,
PluginConfiguration,
PropagationConfiguration,
ScanTargetConfiguration,
TCPScanConfiguration,
)
class CustomPBAConfigurationSchema(Schema):
linux_command = fields.Str()
linux_filename = fields.Str()
windows_command = fields.Str()
windows_filename = fields.Str()
@post_load
def _make_custom_pba_configuration(self, data, **kwargs):
return CustomPBAConfiguration(**data)
class PluginConfigurationSchema(Schema):
name = fields.Str()
options = fields.Mapping()
@post_load
def _make_plugin_configuration(self, data, **kwargs):
return PluginConfiguration(**data)
class ScanTargetConfigurationSchema(Schema):
blocked_ips = fields.List(fields.Str())
inaccessible_subnets = fields.List(fields.Str())
local_network_scan = fields.Bool()
subnets = fields.List(fields.Str())
@post_load
def _make_scan_target_configuration(self, data, **kwargs):
return ScanTargetConfiguration(**data)
class ICMPScanConfigurationSchema(Schema):
timeout = fields.Float()
@post_load
def _make_icmp_scan_configuration(self, data, **kwargs):
return ICMPScanConfiguration(**data)
class TCPScanConfigurationSchema(Schema):
timeout = fields.Float()
ports = fields.List(fields.Int())
@post_load
def _make_tcp_scan_configuration(self, data, **kwargs):
return TCPScanConfiguration(**data)
class NetworkScanConfigurationSchema(Schema):
tcp = fields.Nested(TCPScanConfigurationSchema)
icmp = fields.Nested(ICMPScanConfigurationSchema)
fingerprinters = fields.List(fields.Nested(PluginConfigurationSchema))
targets = fields.Nested(ScanTargetConfigurationSchema)
@post_load
def _make_network_scan_configuration(self, data, **kwargs):
return NetworkScanConfiguration(**data)
class ExploitationOptionsConfigurationSchema(Schema):
http_ports = fields.List(fields.Int())
@post_load
def _make_exploitation_options_configuration(self, data, **kwargs):
return ExploitationOptionsConfiguration(**data)
class ExploitationConfigurationSchema(Schema):
options = fields.Nested(ExploitationOptionsConfigurationSchema)
brute_force = fields.List(fields.Nested(PluginConfigurationSchema))
vulnerability = fields.List(fields.Nested(PluginConfigurationSchema))
@post_load
def _make_exploitation_options_configuration(self, data, **kwargs):
return ExploitationConfiguration(**data)
class PropagationConfigurationSchema(Schema):
maximum_depth = fields.Int()
network_scan = fields.Nested(NetworkScanConfigurationSchema)
exploitation = fields.Nested(ExploitationConfigurationSchema)
@post_load
def _make_propagation_configuration(self, data, **kwargs):
return PropagationConfiguration(**data)

View File

@ -0,0 +1,62 @@
from dataclasses import dataclass
from typing import Dict, List
@dataclass(frozen=True)
class CustomPBAConfiguration:
linux_command: str
linux_filename: str
windows_command: str
windows_filename: str
@dataclass(frozen=True)
class PluginConfiguration:
name: str
options: Dict
@dataclass(frozen=True)
class ScanTargetConfiguration:
blocked_ips: List[str]
inaccessible_subnets: List[str]
local_network_scan: bool
subnets: List[str]
@dataclass(frozen=True)
class ICMPScanConfiguration:
timeout: float
@dataclass(frozen=True)
class TCPScanConfiguration:
timeout: float
ports: List[int]
@dataclass(frozen=True)
class NetworkScanConfiguration:
tcp: TCPScanConfiguration
icmp: ICMPScanConfiguration
fingerprinters: List[PluginConfiguration]
targets: ScanTargetConfiguration
@dataclass(frozen=True)
class ExploitationOptionsConfiguration:
http_ports: List[int]
@dataclass(frozen=True)
class ExploitationConfiguration:
options: ExploitationOptionsConfiguration
brute_force: List[PluginConfiguration]
vulnerability: List[PluginConfiguration]
@dataclass(frozen=True)
class PropagationConfiguration:
maximum_depth: int
network_scan: NetworkScanConfiguration
exploitation: ExploitationConfiguration

View File

@ -0,0 +1,114 @@
from . import AgentConfiguration
from .agent_sub_configurations import (
CustomPBAConfiguration,
ExploitationConfiguration,
ExploitationOptionsConfiguration,
ICMPScanConfiguration,
NetworkScanConfiguration,
PluginConfiguration,
PropagationConfiguration,
ScanTargetConfiguration,
TCPScanConfiguration,
)
PBAS = [
"CommunicateAsBackdoorUser",
"ModifyShellStartupFiles",
"HiddenFiles",
"TrapCommand",
"ChangeSetuidSetgid",
"ScheduleJobs",
"Timestomping",
"AccountDiscovery",
"ProcessListCollection",
]
CREDENTIAL_COLLECTORS = ["MimikatzCollector", "SSHCollector"]
PBA_CONFIGURATION = [PluginConfiguration(pba, {}) for pba in PBAS]
CREDENTIAL_COLLECTOR_CONFIGURATION = [
PluginConfiguration(collector, {}) for collector in CREDENTIAL_COLLECTORS
]
RANSOMWARE_OPTIONS = {
"encryption": {
"enabled": True,
"directories": {"linux_target_dir": "", "windows_target_dir": ""},
},
"other_behaviors": {"readme": True},
}
PAYLOAD_CONFIGURATION = [PluginConfiguration("ransomware", RANSOMWARE_OPTIONS)]
CUSTOM_PBA_CONFIGURATION = CustomPBAConfiguration(
linux_command="", linux_filename="", windows_command="", windows_filename=""
)
TCP_PORTS = [
22,
80,
135,
443,
445,
2222,
3306,
3389,
5985,
5986,
7001,
8008,
8080,
8088,
8983,
9200,
9600,
]
TCP_SCAN_CONFIGURATION = TCPScanConfiguration(timeout=3.0, ports=TCP_PORTS)
ICMP_CONFIGURATION = ICMPScanConfiguration(timeout=1.0)
HTTP_PORTS = [80, 443, 7001, 8008, 8080, 8983, 9200, 9600]
FINGERPRINTERS = [
PluginConfiguration("elastic", {}),
PluginConfiguration("http", {"http_ports": HTTP_PORTS}),
PluginConfiguration("mssql", {}),
PluginConfiguration("smb", {}),
PluginConfiguration("ssh", {}),
]
SCAN_TARGET_CONFIGURATION = ScanTargetConfiguration([], [], True, [])
NETWORK_SCAN_CONFIGURATION = NetworkScanConfiguration(
TCP_SCAN_CONFIGURATION, ICMP_CONFIGURATION, FINGERPRINTERS, SCAN_TARGET_CONFIGURATION
)
EXPLOITATION_OPTIONS_CONFIGURATION = ExploitationOptionsConfiguration(HTTP_PORTS)
BRUTE_FORCE_EXPLOITERS = [
PluginConfiguration("MSSQLExploiter", {}),
PluginConfiguration("PowerShellExploiter", {}),
PluginConfiguration("SSHExploiter", {}),
PluginConfiguration("SmbExploiter", {"smb_download_timeout": 30}),
PluginConfiguration("WmiExploiter", {"smb_download_timeout": 30}),
]
VULNERABILITY_EXPLOITERS = [
PluginConfiguration("Log4ShellExploiter", {}),
PluginConfiguration("HadoopExploiter", {}),
]
EXPLOITATION_CONFIGURATION = ExploitationConfiguration(
EXPLOITATION_OPTIONS_CONFIGURATION, BRUTE_FORCE_EXPLOITERS, VULNERABILITY_EXPLOITERS
)
PROPAGATION_CONFIGURATION = PropagationConfiguration(
maximum_depth=2,
network_scan=NETWORK_SCAN_CONFIGURATION,
exploitation=EXPLOITATION_CONFIGURATION,
)
DEFAULT_AGENT_CONFIGURATION = AgentConfiguration(
keep_tunnel_open_time=30,
custom_pbas=CUSTOM_PBA_CONFIGURATION,
post_breach_actions=PBA_CONFIGURATION,
credential_collectors=CREDENTIAL_COLLECTOR_CONFIGURATION,
payloads=PAYLOAD_CONFIGURATION,
propagation=PROPAGATION_CONFIGURATION,
)

View File

@ -0,0 +1,13 @@
from enum import Enum
class OperatingSystems(Enum):
"""
An Enum representing all supported operating systems
This Enum represents all operating systems that Infection Monkey supports. The value of each
member is the member's name in all lower-case characters.
"""
LINUX = "linux"
WINDOWS = "windows"

View File

@ -38,5 +38,7 @@ class DomainControllerNameFetchError(FailedExploitationError):
"""Raise on failed attempt to extract domain controller's name"""
# TODO: This has been replaced by common.configuration.InvalidConfigurationError. Use that error
# instead and remove this one.
class InvalidConfigurationError(Exception):
"""Raise when configuration is invalid"""

View File

@ -24,6 +24,8 @@ pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requ
pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified
pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows
paramiko = {editable = true, ref = "2.10.3.dev1", git = "https://github.com/VakarisZ/paramiko.git"}
marshmallow = "*"
marshmallow-enum = "*"
[dev-packages]
ldap3 = "*"

View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c1c28510b728242624129b39bd9ace9ba2629061bf64226cb43f1f857fc87212"
"sha256": "202bc4667be3a990bdf39be19ce2ddbacc0b3189810d89f75464ad361c9142b2"
},
"pipfile-spec": 6,
"requires": {
@ -71,26 +71,28 @@
},
"bcrypt": {
"hashes": [
"sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d",
"sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29",
"sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7",
"sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34",
"sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7",
"sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55",
"sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd",
"sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6",
"sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1",
"sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"
"sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521",
"sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb",
"sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e",
"sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26",
"sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a",
"sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e",
"sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa",
"sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129",
"sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb",
"sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40",
"sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"
],
"markers": "python_version >= '3.6'",
"version": "==3.2.0"
"version": "==3.2.2"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
],
"version": "==2021.10.8"
"markers": "python_version >= '3.6'",
"version": "==2022.6.15"
},
"cffi": {
"hashes": [
@ -160,24 +162,24 @@
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12"
},
"click": {
"hashes": [
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.2"
"version": "==8.1.3"
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
"sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da",
"sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.4"
"version": "==0.4.5"
},
"constantly": {
"hashes": [
@ -188,29 +190,31 @@
},
"cryptography": {
"hashes": [
"sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b",
"sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51",
"sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7",
"sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d",
"sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6",
"sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29",
"sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9",
"sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf",
"sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815",
"sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf",
"sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85",
"sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77",
"sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86",
"sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb",
"sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e",
"sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0",
"sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3",
"sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84",
"sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2",
"sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6"
"sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804",
"sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178",
"sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717",
"sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982",
"sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004",
"sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe",
"sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452",
"sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336",
"sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4",
"sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15",
"sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d",
"sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c",
"sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0",
"sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06",
"sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9",
"sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1",
"sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023",
"sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de",
"sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f",
"sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181",
"sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e",
"sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"
],
"markers": "python_version >= '3.6'",
"version": "==36.0.2"
"version": "==37.0.2"
},
"dnspython": {
"hashes": [
@ -222,11 +226,11 @@
},
"flask": {
"hashes": [
"sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264",
"sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8"
"sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477",
"sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
"version": "==2.1.2"
},
"future": {
"hashes": [
@ -247,23 +251,23 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"markers": "python_full_version >= '3.5.0'",
"version": "==3.3"
},
"impacket": {
"hashes": [
"sha256:18d557d387f4914fafa739813b9172bc3f8bd9c036e93bf589a8e0ebb7304bba"
"sha256:b8eb020a2cbb47146669cfe31c64bb2e7d6499d049c493d6418b9716f5c74583"
],
"index": "pypi",
"version": "==0.9.24"
"version": "==0.10.0"
},
"importlib-metadata": {
"hashes": [
"sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6",
"sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"
"sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700",
"sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"
],
"markers": "python_version < '3.8'",
"version": "==4.11.3"
"version": "==4.11.4"
},
"incremental": {
"hashes": [
@ -290,11 +294,11 @@
},
"jinja2": {
"hashes": [
"sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119",
"sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.1"
"version": "==3.1.2"
},
"ldap3": {
"hashes": [
@ -368,6 +372,22 @@
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
},
"marshmallow": {
"hashes": [
"sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1",
"sha256:a762c1d8b2bcb0e5c8e964850d03f9f3bffd6a12b626f3c14b9d6b1841999af5"
],
"index": "pypi",
"version": "==3.16.0"
},
"marshmallow-enum": {
"hashes": [
"sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58",
"sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"
],
"index": "pypi",
"version": "==1.5.1"
},
"minidump": {
"hashes": [
"sha256:6a9d2152f76ae633c609e09b48b42f55bd5a6b65f920dbbec756e5d9134a6201",
@ -442,6 +462,14 @@
],
"version": "==1.3.0"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"paramiko": {
"editable": true,
"git": "https://github.com/VakarisZ/paramiko.git",
@ -456,10 +484,11 @@
},
"pefile": {
"hashes": [
"sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a"
"sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"
],
"index": "pypi",
"markers": "sys_platform == 'win32'",
"version": "==2021.9.3"
"version": "==2022.5.30"
},
"prompt-toolkit": {
"hashes": [
@ -471,41 +500,41 @@
},
"psutil": {
"hashes": [
"sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5",
"sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a",
"sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4",
"sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841",
"sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d",
"sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d",
"sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0",
"sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845",
"sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf",
"sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b",
"sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07",
"sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618",
"sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2",
"sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd",
"sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666",
"sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce",
"sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3",
"sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d",
"sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25",
"sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492",
"sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b",
"sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d",
"sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2",
"sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203",
"sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2",
"sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94",
"sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9",
"sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64",
"sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56",
"sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3",
"sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c",
"sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3"
"sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685",
"sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc",
"sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36",
"sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1",
"sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329",
"sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81",
"sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de",
"sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4",
"sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574",
"sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237",
"sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22",
"sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b",
"sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0",
"sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954",
"sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021",
"sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537",
"sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87",
"sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0",
"sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc",
"sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af",
"sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4",
"sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453",
"sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689",
"sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8",
"sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680",
"sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e",
"sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9",
"sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b",
"sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d",
"sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2",
"sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5",
"sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"
],
"index": "pypi",
"version": "==5.9.0"
"version": "==5.9.1"
},
"pyasn1": {
"hashes": [
@ -603,11 +632,11 @@
},
"pyinstaller-hooks-contrib": {
"hashes": [
"sha256:9765e68552803327d58f6c5eca970bb245b7cdf073e2f912a2a3cb50360bc2d8",
"sha256:9fa4ca03d058cba676c3cc16005076ce6a529f144c08b87c69998625fbd84e0a"
"sha256:5fdb97dcae177955db7ab27840cba97b89dc0c7f4fd9142bba0f9b8d8df85c48",
"sha256:6675634279cfe9e475580fb310c3d557037baefb065e6cb5a69a124361b926fd"
],
"markers": "python_version >= '3.7'",
"version": "==2022.3"
"version": "==2022.7"
},
"pymssql": {
"hashes": [
@ -670,11 +699,11 @@
},
"pyparsing": {
"hashes": [
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
"version": "==3.0.9"
},
"pypsrp": {
"hashes": [
@ -694,48 +723,66 @@
},
"pysmb": {
"hashes": [
"sha256:298605b8f467ce15b412caaf9af331c135e88fa2172333af14b1b2916361cb6b"
"sha256:3b07db16217465039d0c25694c0705b83663ca82259e209f3566d577536a7395"
],
"index": "pypi",
"version": "==1.2.7"
"version": "==1.2.8"
},
"pyspnego": {
"hashes": [
"sha256:05438a4e3e1526134bc2d72213417a06a2c3010f5b7271f3122e635e523c3790",
"sha256:12e4da1cbbbd645c0624699a1d99f734161cb9095e9f1fc1c1982ed1b7a44abe",
"sha256:185e0c576cde30d8853d9ea1d69c32cb93e98423934263d6c067bec7adc7dc4f",
"sha256:3361027e7e86de6b784791e09a7b2ba73d06c0be40f027a7be09e45fc92325a5",
"sha256:4971fb166dc9821c98d31d698722d48d0066f1bc63beff8bf3d2a2e60fe507d1",
"sha256:58d352d901baab754f63cb0da790c1f798605eb634f7f922df9bb6822d3de3c5",
"sha256:77b7c75bed737f24989aab453b9b8cd1c1512dfc5bed7a303a1cb1156fd59959",
"sha256:adf2f3e09bc4751c06fab1fedfe734af7f232d79927c753d8981f75a25f791ec",
"sha256:c6993ee6bcfe0036d6246324fcb7975daed858a476bfc7bf1d9334911d3dfca2",
"sha256:e21fc7283caa16761d46bea54e78cbfe3177c21e3b2d17d9ef213edcd86e1250",
"sha256:f05f1a6316a9baeaef243c9420d995c3dc34cfc91841f17db0c793e3fe557728",
"sha256:fe8b2a0d7468d904c61ae63275f8234eb055767aaaba66f6d58d86f47a25aa8e"
"sha256:1fed228edc4b1730844da8237b90489be28c55681cf3934cd04fceb2253e55bf",
"sha256:25fbc90fc6bd16881480316739bab820cc91364765e46340da17f861f89691f1",
"sha256:274b3a2d37e45ad4567bc5754be04b5fefad3f7cdea7d205f739d8a26b5a9189",
"sha256:36db7ec38023a23a545114dfd23825639571f135c72fb4b13a1ed559a0a4d93c",
"sha256:3b1ff3c1d5588b66f8e4ebafa3079a7bf0bdcc6fb144b944c5a101e688a5a280",
"sha256:4b9bda51bd964f40322aa1b33dcfc5d68f23b0680b4b5158175f2e9a04119aa9",
"sha256:5d6d91e35ee63a5de30eb70716bf25274bf16c2c472b046dd21fad60fe63b0b6",
"sha256:7562bc640bf402bb2849f325b0bb41260bd2c0cb06e38b9a8c6f7021b452c873",
"sha256:9c5bdb9f0207e2ce9e5410ee2205bf016755712018534c711ae6c1daff7fa7db",
"sha256:a5c135d20819db3c48f65054d648317f369a61b7b22dc17b9e5ec9c0169541a0",
"sha256:bd95633e7dce69e267579bdbe992fc081a14310236b4e84c3d179b1cf6439ca5",
"sha256:eb41b970dbda0dfe07b1da6fc83fe9f534a66d8dea38c06c0155377697407d9a"
],
"markers": "python_version >= '3.6'",
"version": "==0.5.1"
"version": "==0.5.2"
},
"pywin32": {
"sys_platform": "== 'win32'",
"version": "==303"
"hashes": [
"sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc",
"sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269",
"sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3",
"sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48",
"sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4",
"sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96",
"sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df",
"sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1",
"sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43",
"sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9",
"sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f",
"sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988",
"sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e",
"sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5"
],
"index": "pypi",
"markers": "sys_platform == 'win32'",
"version": "==304"
},
"pywin32-ctypes": {
"hashes": [
"sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
"sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
],
"index": "pypi",
"markers": "sys_platform == 'win32'",
"version": "==0.2.0"
},
"requests": {
"hashes": [
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f",
"sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"
],
"index": "pypi",
"version": "==2.27.1"
"version": "==2.28.0"
},
"service-identity": {
"hashes": [
@ -746,11 +793,11 @@
},
"setuptools": {
"hashes": [
"sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8",
"sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592"
"sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77",
"sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91"
],
"markers": "python_version >= '3.7'",
"version": "==62.1.0"
"version": "==62.4.0"
},
"six": {
"hashes": [
@ -770,30 +817,48 @@
},
"twisted": {
"extras": [
"tls"
],
"hashes": [
"sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680",
"sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2"
],
"index": "pypi",
"markers": "python_full_version >= '3.6.7'",
"version": "==22.4.0"
},
"twisted-iocpsupport": {
"hashes": [
"sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41",
"sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d",
"sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9",
"sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf",
"sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323",
"sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32",
"sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4",
"sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f",
"sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546",
"sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878",
"sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565",
"sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415"
],
"markers": "platform_system == 'Windows'",
"version": "==1.0.2"
},
"typing-extensions": {
"hashes": [
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
"sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708",
"sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"
],
"index": "pypi",
"version": "==4.1.1"
"version": "==4.2.0"
},
"unicrypto": {
"hashes": [
"sha256:69240f260493346861e639697e2fda245f14656c7df20ae6d9e6b1bb36acb29e",
"sha256:822bbf18ca6bc17f98c3029470bd8898e51fcb6a7716f6b1c95ed0bf9e0e4da5"
"sha256:0487f9dd9009c326ee9531a79412ae18ad673425a1c800d64947b96fdeb04cdf",
"sha256:fab49ee41926bb31be49552aa135f7aedc04436b49c8fe326d7b6a823810575e"
],
"markers": "python_version >= '3.6'",
"version": "==0.0.5"
"version": "==0.0.8"
},
"urllib3": {
"hashes": [
@ -812,27 +877,27 @@
},
"werkzeug": {
"hashes": [
"sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6",
"sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74"
"sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6",
"sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
"version": "==2.1.2"
},
"winacl": {
"hashes": [
"sha256:187b4394ef247806f50e1d8320bdb9e33ad1f759d9e61e2e391b97b9adf5f58a",
"sha256:949a66b0f46015c8cf8d9c1bfdb3a5174e70c28ae1b096eb778bc2983ea7ce50"
"sha256:1bac567a9d21300082aa2246bb0f94a591fca8e218e163bab18df0e32eefea06",
"sha256:a0b76a327fd337d5ee707ccff95222e6b6ecaa6d887613a1c3d3437ce0be1d4d"
],
"markers": "python_version >= '3.6'",
"version": "==0.1.2"
"version": "==0.1.3"
},
"winsspi": {
"hashes": [
"sha256:a2ad9c0f6d70f6e0e0d1f54b8582054c62d8a09f346b5ccaf55da68628ca10e1",
"sha256:a64624a25fc2d3663a2c5376c5291f3c7531e9c8051571de9ca9db8bf25746c2"
"sha256:2f5a8d2c4b9f459144426909e26a74e550512e23b6cf9af52c2a00003c7c3fdb",
"sha256:59b7c7595f91414528cfd80c6cfc77ec6f5e4e28185ebd6418f8368ddc7aca82"
],
"markers": "python_version >= '3.6'",
"version": "==0.0.9"
"version": "==0.0.10"
},
"winsys-3.x": {
"hashes": [
@ -846,6 +911,7 @@
"sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942",
"sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6"
],
"index": "pypi",
"markers": "sys_platform == 'win32'",
"version": "==1.5.1"
},

View File

@ -1,69 +1,4 @@
import uuid
from abc import ABCMeta
# TODO: Find a better place for this
GUID = str(uuid.getnode())
SENSITIVE_FIELDS = [
"exploit_password_list",
"exploit_user_list",
"exploit_ssh_keys",
]
LOCAL_CONFIG_VARS = ["name", "id", "max_depth"]
HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden"
class Configuration(object):
def from_kv(self, formatted_data):
for key, value in list(formatted_data.items()):
if key.startswith("_"):
continue
if key in LOCAL_CONFIG_VARS:
continue
if hasattr(self, key):
setattr(self, key, value)
if not self.max_depth:
self.max_depth = self.depth
@staticmethod
def hide_sensitive_info(config_dict):
for field in SENSITIVE_FIELDS:
config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT
return config_dict
def as_dict(self):
result = {}
for key in dir(Configuration):
if key.startswith("_"):
continue
try:
value = getattr(self, key)
except AttributeError:
continue
val_type = type(value)
if callable(value):
continue
if val_type in (type, ABCMeta):
value = value.__name__
elif val_type is tuple or val_type is list:
if len(value) != 0 and type(value[0]) in (type, ABCMeta):
value = val_type([x.__name__ for x in value])
result[key] = value
return result
###########################
# monkey config
###########################
# depth of propagation
depth = 0
max_depth = None
keep_tunnel_open_time = 30
WormConfiguration = Configuration()

View File

@ -1,7 +1,6 @@
import json
import logging
import platform
from pprint import pformat
from socket import gethostname
from typing import Mapping, Optional
@ -10,7 +9,7 @@ from requests.exceptions import ConnectionError
import infection_monkey.tunnel as tunnel
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
from infection_monkey.config import GUID, WormConfiguration
from infection_monkey.config import GUID
from infection_monkey.network.info import get_host_subnets, local_ips
from infection_monkey.transport.http import HTTPConnectProxy
from infection_monkey.transport.tcp import TcpProxy
@ -151,38 +150,7 @@ class ControlClient:
except Exception as exc:
logger.warning(f"Error connecting to control server {self.server_address}: {exc}")
def load_control_config(self):
if not self.server_address:
return
try:
reply = requests.get( # noqa: DUO123
f"https://{self.server_address}/api/agent/",
verify=False,
proxies=self.proxies,
timeout=MEDIUM_REQUEST_TIMEOUT,
)
except Exception as exc:
logger.warning(f"Error connecting to control server {self.server_address}: {exc}")
return
try:
WormConfiguration.from_kv(reply.json().get("config"))
formatted_config = pformat(
WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict())
)
logger.info(f"New configuration was loaded from server:\n{formatted_config}")
except Exception as exc:
# we don't continue with default conf here because it might be dangerous
logger.error(
"Error parsing JSON reply from control server %s (%s): %s",
self.server_address,
reply._content,
exc,
)
raise Exception("Couldn't load from from server's configuration, aborting. %s" % exc)
def create_control_tunnel(self):
def create_control_tunnel(self, keep_tunnel_open_time: int):
if not self.server_address:
return None
@ -200,7 +168,7 @@ class ControlClient:
return tunnel.MonkeyTunnel(
proxy_class,
keep_tunnel_open_time=WormConfiguration.keep_tunnel_open_time,
keep_tunnel_open_time=keep_tunnel_open_time,
target_addr=target_addr,
target_port=target_port,
)

View File

@ -5,6 +5,7 @@ from typing import Mapping
import requests
from common import OperatingSystems
from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT
from . import IAgentRepository
@ -22,18 +23,22 @@ class CachingAgentRepository(IAgentRepository):
self._proxies = proxies
self._lock = threading.Lock()
def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO:
def get_agent_binary(
self, operating_system: OperatingSystems, architecture: str = None
) -> io.BytesIO:
# If multiple calls to get_agent_binary() are made simultaneously before the result of
# _download_binary_from_island() is cached, then multiple requests will be sent to the
# island. Add a mutex in front of the call to _download_agent_binary_from_island() so
# that only one request per OS will be sent to the island.
with self._lock:
return io.BytesIO(self._download_binary_from_island(os))
return io.BytesIO(self._download_binary_from_island(operating_system))
@lru_cache(maxsize=None)
def _download_binary_from_island(self, os: str) -> bytes:
def _download_binary_from_island(self, operating_system: OperatingSystems) -> bytes:
os_name = operating_system.value
response = requests.get( # noqa: DUO123
f"{self._island_url}/api/agent-binaries/{os}",
f"{self._island_url}/api/agent-binaries/{os_name}",
verify=False,
proxies=self._proxies,
timeout=MEDIUM_REQUEST_TIMEOUT,

View File

@ -105,10 +105,10 @@ class HadoopExploiter(WebRCE):
def _build_command(self, path, http_path):
# Build command to execute
monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1)
if "linux" in self.host.os["type"]:
base_command = HADOOP_LINUX_COMMAND
else:
if self.host.is_windows():
base_command = HADOOP_WINDOWS_COMMAND
else:
base_command = HADOOP_LINUX_COMMAND
return base_command % {
"monkey_path": path,

View File

@ -1,6 +1,8 @@
import abc
import io
from common import OperatingSystems
# TODO: The Island also has an IAgentRepository with a totally different interface. At the moment,
# the Island and Agent have different needs, but at some point we should unify these.
@ -13,12 +15,13 @@ class IAgentRepository(metaclass=abc.ABCMeta):
"""
@abc.abstractmethod
def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO:
def get_agent_binary(
self, operating_system: OperatingSystems, architecture: str = None
) -> io.BytesIO:
"""
Retrieve the appropriate agent binary from the repository.
:param str os: The name of the operating system on which the agent binary will run
:param str architecture: Reserved
:param operating_system: The name of the operating system on which the agent binary will run
:param architecture: Reserved
:return: A file-like object for the requested agent binary
:rtype: io.BytesIO
"""
pass

View File

@ -2,6 +2,7 @@ import logging
import time
from pathlib import PurePath
from common import OperatingSystems
from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT
from common.utils import Timer
from infection_monkey.exploit.log4shell_utils import (
@ -115,10 +116,10 @@ class Log4ShellExploiter(WebRCE):
def _build_command(self, path: PurePath, http_path) -> str:
# Build command to execute
monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1, location=path)
if "linux" in self.host.os["type"]:
base_command = LOG4SHELL_LINUX_COMMAND
else:
if self.host.is_windows():
base_command = LOG4SHELL_WINDOWS_COMMAND
else:
base_command = LOG4SHELL_LINUX_COMMAND
return base_command % {
"monkey_path": path,
@ -128,7 +129,7 @@ class Log4ShellExploiter(WebRCE):
}
def _build_java_class(self, exploit_command: str) -> bytes:
if "linux" in self.host.os["type"]:
if OperatingSystems.LINUX == self.host.os["type"]:
return build_exploit_bytecode(exploit_command, LINUX_EXPLOIT_TEMPLATE_PATH)
else:
return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH)

View File

@ -2,6 +2,7 @@ import logging
from pathlib import Path, PurePath
from typing import List, Optional
from common import OperatingSystems
from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions, get_auth_options
from infection_monkey.exploit.powershell_utils.credentials import (
@ -162,7 +163,7 @@ class PowerShellExploiter(HostExploiter):
temp_monkey_binary_filepath.unlink()
def _create_local_agent_file(self, binary_path):
agent_binary_bytes = self.agent_repository.get_agent_binary("windows")
agent_binary_bytes = self.agent_repository.get_agent_binary(OperatingSystems.WINDOWS)
with open(binary_path, "wb") as f:
f.write(agent_binary_bytes.getvalue())

View File

@ -15,7 +15,7 @@ AGENT_BINARY_PATH_WIN64 = PureWindowsPath(r"C:\Windows\temp\monkey64.exe")
def get_agent_dst_path(host: VictimHost) -> PurePath:
if host.os["type"] == "windows":
if host.is_windows():
path = PureWindowsPath(AGENT_BINARY_PATH_WIN64)
else:
path = PurePosixPath(AGENT_BINARY_PATH_LINUX)

View File

@ -57,6 +57,6 @@ class HTTPTools(object):
httpd.start()
lock.acquire()
return (
"http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(host.os["type"])),
f"http://{local_ip}:{local_port}/{urllib.parse.quote(host.os['type'].value)}",
httpd,
)

View File

@ -3,6 +3,7 @@ from abc import abstractmethod
from posixpath import join
from typing import List, Tuple
from common import OperatingSystems
from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus
from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.tools.http_tools import HTTPTools
@ -162,10 +163,10 @@ class WebRCE(HostExploiter):
def get_command(self, path, http_path, commands):
try:
if "linux" in self.host.os["type"]:
command = commands["linux"]
else:
if self.host.is_windows():
command = commands["windows"]
else:
command = commands["linux"]
# Format command
command = command % {"monkey_path": path, "http_path": http_path}
except KeyError:
@ -326,7 +327,7 @@ class WebRCE(HostExploiter):
:return: response, False if failed and True if permission change is not needed
"""
logger.info("Changing monkey's permissions")
if "windows" in self.host.os["type"]:
if self.host.is_windows():
logger.info("Permission change not required for windows")
return True
if not command:
@ -411,13 +412,14 @@ class WebRCE(HostExploiter):
:return: Default monkey's destination path for corresponding host or False if failed.
"""
if not self.host.os.get("type") or (
self.host.os["type"] != "linux" and self.host.os["type"] != "windows"
self.host.os["type"] != OperatingSystems.LINUX
and self.host.os["type"] != OperatingSystems.WINDOWS
):
logger.error("Target's OS was either unidentified or not supported. Aborting")
return False
if self.host.os["type"] == "linux":
if self.host.os["type"] == OperatingSystems.LINUX:
return DROPPER_TARGET_PATH_LINUX
if self.host.os["type"] == "windows":
if self.host.os["type"] == OperatingSystems.WINDOWS:
return DROPPER_TARGET_PATH_WIN64
def get_target_url(self):

View File

@ -1,5 +1,7 @@
import abc
from common.configuration import AgentConfiguration
class IControlChannel(metaclass=abc.ABCMeta):
@abc.abstractmethod
@ -11,10 +13,10 @@ class IControlChannel(metaclass=abc.ABCMeta):
"""
@abc.abstractmethod
def get_config(self) -> dict:
def get_config(self) -> AgentConfiguration:
"""
:return: A dictionary containing Agent Configuration
:rtype: dict
:return: An AgentConfiguration object
:rtype: AgentConfiguration
"""
pass

View File

@ -5,13 +5,11 @@ import os
import sys
import traceback
from multiprocessing import freeze_support
from pprint import pformat
# dummy import for pyinstaller
# noinspection PyUnresolvedReferences
import infection_monkey.post_breach # noqa: F401
from common.version import get_version
from infection_monkey.config import WormConfiguration
from infection_monkey.dropper import MonkeyDrops
from infection_monkey.model import DROPPER_ARG, MONKEY_ARG
from infection_monkey.monkey import InfectionMonkey
@ -57,9 +55,6 @@ def main():
mode_args, mode_specific_args = arg_parser.parse_known_args()
mode = mode_args.mode
formatted_config = pformat(WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()))
print(f"Loaded Configuration:\n{formatted_config}")
try:
if MONKEY_ARG == mode:
log_path = get_agent_log_path()

View File

@ -1,8 +1,9 @@
import logging
import threading
import time
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple
from typing import Any, Callable, Iterable, List, Optional
from common.configuration import CustomPBAConfiguration, PluginConfiguration
from common.utils import Timer
from infection_monkey.credential_store import ICredentialsStore
from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError
@ -13,7 +14,7 @@ from infection_monkey.network import NetworkInterface
from infection_monkey.telemetry.credentials_telem import CredentialsTelem
from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
from infection_monkey.utils.propagation import should_propagate
from infection_monkey.utils.propagation import maximum_depth_reached
from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter
from . import Exploiter, IPScanner, Propagator
@ -111,7 +112,7 @@ class AutomatedMaster(IMaster):
time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC)
@staticmethod
def _try_communicate_with_island(fn: Callable[[], Any], max_tries: int):
def _try_communicate_with_island(fn: Callable[[], Any], max_tries: int) -> Any:
tries = 0
while tries < max_tries:
try:
@ -141,7 +142,7 @@ class AutomatedMaster(IMaster):
try:
config = AutomatedMaster._try_communicate_with_island(
self._control_channel.get_config, CHECK_FOR_CONFIG_COUNT
)["config"]
)
except IslandCommunicationError as e:
logger.error(f"An error occurred while fetching configuration: {e}")
return
@ -150,7 +151,7 @@ class AutomatedMaster(IMaster):
target=self._run_plugins,
name="CredentialCollectorThread",
args=(
config["credential_collectors"],
config.credential_collectors,
"credential collector",
self._collect_credentials,
),
@ -158,7 +159,7 @@ class AutomatedMaster(IMaster):
pba_thread = create_daemon_thread(
target=self._run_pbas,
name="PBAThread",
args=(config["post_breach_actions"].items(), self._run_pba, config["custom_pbas"]),
args=(config.post_breach_actions, self._run_pba, config.custom_pbas),
)
credential_collector_thread.start()
@ -173,52 +174,56 @@ class AutomatedMaster(IMaster):
current_depth = self._current_depth if self._current_depth is not None else 0
logger.info(f"Current depth is {current_depth}")
if should_propagate(self._control_channel.get_config(), self._current_depth):
self._propagator.propagate(config["propagation"], current_depth, self._stop)
if maximum_depth_reached(config.propagation.maximum_depth, current_depth):
self._propagator.propagate(config.propagation, current_depth, self._stop)
else:
logger.info("Skipping propagation: maximum depth reached")
payload_thread = create_daemon_thread(
target=self._run_plugins,
name="PayloadThread",
args=(config["payloads"].items(), "payload", self._run_payload),
args=(config.payloads, "payload", self._run_payload),
)
payload_thread.start()
payload_thread.join()
pba_thread.join()
def _collect_credentials(self, collector: str):
credentials = self._puppet.run_credential_collector(collector, {})
def _collect_credentials(self, collector: PluginConfiguration):
credentials = self._puppet.run_credential_collector(collector.name, collector.options)
if credentials:
self._telemetry_messenger.send_telemetry(CredentialsTelem(credentials))
else:
logger.debug(f"No credentials were collected by {collector}")
def _run_pba(self, pba: Tuple[str, Dict]):
name = pba[0]
options = pba[1]
for pba_data in self._puppet.run_pba(name, options):
def _run_pba(self, pba: PluginConfiguration):
for pba_data in self._puppet.run_pba(pba.name, pba.options):
self._telemetry_messenger.send_telemetry(PostBreachTelem(pba_data))
def _run_payload(self, payload: Tuple[str, Dict]):
name = payload[0]
options = payload[1]
self._puppet.run_payload(name, options, self._stop)
def _run_payload(self, payload: PluginConfiguration):
self._puppet.run_payload(payload.name, payload.options, self._stop)
def _run_pbas(
self, plugins: Iterable[Any], callback: Callable[[Any], None], custom_pba_options: Mapping
self,
plugins: Iterable[PluginConfiguration],
callback: Callable[[Any], None],
custom_pba_options: CustomPBAConfiguration,
):
self._run_plugins(plugins, "post-breach action", callback)
if custom_pba_is_enabled(custom_pba_options):
self._run_plugins([("CustomPBA", custom_pba_options)], "post-breach action", callback)
self._run_plugins(
[PluginConfiguration(name="CustomPBA", options=custom_pba_options.__dict__)],
"post-breach action",
callback,
)
def _run_plugins(
self, plugins: Iterable[Any], plugin_type: str, callback: Callable[[Any], None]
self,
plugins: Iterable[PluginConfiguration],
plugin_type: str,
callback: Callable[[Any], None],
):
logger.info(f"Running {plugin_type}s")
logger.debug(f"Found {len(plugins)} {plugin_type}(s) to run")

View File

@ -1,10 +1,12 @@
import json
import logging
from pprint import pformat
from typing import Mapping
import requests
from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT
from common.configuration import AgentConfiguration
from infection_monkey.custom_types import PropagationCredentials
from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError
@ -47,7 +49,7 @@ class ControlChannel(IControlChannel):
) as e:
raise IslandCommunicationError(e)
def get_config(self) -> dict:
def get_config(self) -> AgentConfiguration:
try:
response = requests.get( # noqa: DUO123
f"https://{self._control_channel_server}/api/agent",
@ -57,7 +59,10 @@ class ControlChannel(IControlChannel):
)
response.raise_for_status()
return json.loads(response.content.decode())
config_dict = json.loads(response.text)["config"]
logger.debug(f"Received configuration:\n{pformat(json.loads(response.text))}")
return AgentConfiguration.from_mapping(config_dict)
except (
json.JSONDecodeError,
requests.exceptions.ConnectionError,

View File

@ -5,8 +5,13 @@ from copy import deepcopy
from itertools import chain
from queue import Queue
from threading import Event
from typing import Callable, Dict, List, Mapping
from typing import Callable, Dict, Sequence
from common import OperatingSystems
from common.configuration.agent_sub_configurations import (
ExploitationConfiguration,
PluginConfiguration,
)
from infection_monkey.custom_types import PropagationCredentials
from infection_monkey.i_puppet import ExploiterResultData, IPuppet
from infection_monkey.model import VictimHost
@ -20,6 +25,18 @@ ExploiterName = str
Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None]
SUPPORTED_OS = {
"HadoopExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS],
"Log4ShellExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS],
"MSSQLExploiter": [OperatingSystems.WINDOWS],
"PowerShellExploiter": [OperatingSystems.WINDOWS],
"SSHExploiter": [OperatingSystems.LINUX],
"SmbExploiter": [OperatingSystems.WINDOWS],
"WmiExploiter": [OperatingSystems.WINDOWS],
"ZerologonExploiter": [OperatingSystems.WINDOWS],
}
class Exploiter:
def __init__(
self,
@ -33,7 +50,7 @@ class Exploiter:
def exploit_hosts(
self,
exploiter_config: Dict,
exploiter_config: ExploitationConfiguration,
hosts_to_exploit: Queue,
current_depth: int,
results_callback: Callback,
@ -43,7 +60,7 @@ class Exploiter:
exploiters_to_run = self._process_exploiter_config(exploiter_config)
logger.debug(
"Agent is configured to run the following exploiters in order: "
f"{', '.join([e['name'] for e in exploiters_to_run])}"
f"{', '.join([e.name for e in exploiters_to_run])}"
)
exploit_args = (
@ -62,24 +79,26 @@ class Exploiter:
)
@staticmethod
def _process_exploiter_config(exploiter_config: Mapping) -> List[Mapping]:
def _process_exploiter_config(
exploiter_config: ExploitationConfiguration,
) -> Sequence[PluginConfiguration]:
# Run vulnerability exploiters before brute force exploiters to minimize the effect of
# account lockout due to invalid credentials
ordered_exploiters = chain(
exploiter_config["vulnerability"], exploiter_config["brute_force"]
)
ordered_exploiters = chain(exploiter_config.vulnerability, exploiter_config.brute_force)
exploiters_to_run = list(deepcopy(ordered_exploiters))
extended_exploiters = []
for exploiter in exploiters_to_run:
# This order allows exploiter-specific options to
# override general options for all exploiters.
exploiter["options"] = {**exploiter_config["options"], **exploiter["options"]}
options = {**exploiter_config.options.__dict__, **exploiter.options}
extended_exploiters.append(PluginConfiguration(exploiter.name, options))
return exploiters_to_run
return extended_exploiters
def _exploit_hosts_on_queue(
self,
exploiters_to_run: List[Dict],
exploiters_to_run: Sequence[PluginConfiguration],
hosts_to_exploit: Queue,
current_depth: int,
results_callback: Callback,
@ -106,7 +125,7 @@ class Exploiter:
def _run_all_exploiters(
self,
exploiters_to_run: List[Dict],
exploiters_to_run: Sequence[PluginConfiguration],
victim_host: VictimHost,
current_depth: int,
results_callback: Callback,
@ -114,11 +133,11 @@ class Exploiter:
):
for exploiter in interruptible_iter(exploiters_to_run, stop):
exploiter_name = exploiter["name"]
exploiter_name = exploiter.name
victim_os = victim_host.os.get("type")
# We want to try all exploiters if the victim's OS is unknown
if victim_os is not None and victim_os not in exploiter["supported_os"]:
if victim_os is not None and victim_os not in SUPPORTED_OS[exploiter_name]:
logger.debug(
f"Skipping {exploiter_name} because it does not support "
f"the victim's OS ({victim_os})"
@ -126,7 +145,7 @@ class Exploiter:
continue
exploiter_results = self._run_exploiter(
exploiter_name, exploiter["options"], victim_host, current_depth, stop
exploiter_name, exploiter.options, victim_host, current_depth, stop
)
results_callback(exploiter_name, victim_host, exploiter_results)

View File

@ -3,8 +3,13 @@ import queue
import threading
from queue import Queue
from threading import Event
from typing import Any, Callable, Dict, List
from typing import Callable, Dict, Sequence
from common.configuration.agent_sub_configurations import (
NetworkScanConfiguration,
PluginConfiguration,
ScanTargetConfiguration,
)
from infection_monkey.i_puppet import (
FingerprintData,
IPuppet,
@ -29,8 +34,8 @@ class IPScanner:
def scan(
self,
addresses_to_scan: List[NetworkAddress],
options: Dict,
addresses_to_scan: Sequence[NetworkAddress],
options: ScanTargetConfiguration,
results_callback: Callback,
stop: Event,
):
@ -49,12 +54,16 @@ class IPScanner:
)
def _scan_addresses(
self, addresses: Queue, options: Dict, results_callback: Callback, stop: Event
self,
addresses: Queue,
options: NetworkScanConfiguration,
results_callback: Callback,
stop: Event,
):
logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}")
icmp_timeout = options["icmp"]["timeout_ms"] / 1000
tcp_timeout = options["tcp"]["timeout_ms"] / 1000
tcp_ports = options["tcp"]["ports"]
logger.debug(f"Starting scan .read -- Thread ID: {threading.get_ident()}")
icmp_timeout = options.icmp.timeout
tcp_timeout = options.tcp.timeout
tcp_ports = options.tcp.ports
try:
while not stop.is_set():
@ -66,7 +75,7 @@ class IPScanner:
fingerprint_data = {}
if IPScanner.port_scan_found_open_port(port_scan_data):
fingerprinters = options["fingerprinters"]
fingerprinters = options.fingerprinters
fingerprint_data = self._run_fingerprinters(
address.ip, fingerprinters, ping_scan_data, port_scan_data, stop
)
@ -90,7 +99,7 @@ class IPScanner:
def _run_fingerprinters(
self,
ip: str,
fingerprinters: List[Dict[str, Any]],
fingerprinters: Sequence[PluginConfiguration],
ping_scan_data: PingScanData,
port_scan_data: Dict[int, PortScanData],
stop: Event,
@ -98,8 +107,8 @@ class IPScanner:
fingerprint_data = {}
for f in interruptible_iter(fingerprinters, stop):
fingerprint_data[f["name"]] = self._puppet.fingerprint(
f["name"], ip, ping_scan_data, port_scan_data, f["options"]
fingerprint_data[f.name] = self._puppet.fingerprint(
f.name, ip, ping_scan_data, port_scan_data, f.options
)
return fingerprint_data

View File

@ -1,13 +1,12 @@
from typing import Dict
from common.configuration import CustomPBAConfiguration
from infection_monkey.utils.environment import is_windows_os
def custom_pba_is_enabled(pba_options: Dict) -> bool:
def custom_pba_is_enabled(pba_options: CustomPBAConfiguration) -> bool:
if not is_windows_os():
if pba_options["linux_command"]:
if pba_options.linux_command:
return True
else:
if pba_options["windows_command"]:
if pba_options.windows_command:
return True
return False

View File

@ -1,8 +1,13 @@
import logging
from queue import Queue
from threading import Event
from typing import Dict, List
from typing import List
from common.configuration import (
NetworkScanConfiguration,
PropagationConfiguration,
ScanTargetConfiguration,
)
from infection_monkey.i_puppet import (
ExploiterResultData,
FingerprintData,
@ -39,14 +44,18 @@ class Propagator:
self._local_network_interfaces = local_network_interfaces
self._hosts_to_exploit = None
def propagate(self, propagation_config: Dict, current_depth: int, stop: Event):
def propagate(
self, propagation_config: PropagationConfiguration, current_depth: int, stop: Event
):
logger.info("Attempting to propagate")
network_scan_completed = Event()
self._hosts_to_exploit = Queue()
scan_thread = create_daemon_thread(
target=self._scan_network, name="PropagatorScanThread", args=(propagation_config, stop)
target=self._scan_network,
name="PropagatorScanThread",
args=(propagation_config.network_scan, stop),
)
exploit_thread = create_daemon_thread(
target=self._exploit_hosts,
@ -64,22 +73,21 @@ class Propagator:
logger.info("Finished attempting to propagate")
def _scan_network(self, propagation_config: Dict, stop: Event):
def _scan_network(self, scan_config: NetworkScanConfiguration, stop: Event):
logger.info("Starting network scan")
target_config = propagation_config["targets"]
scan_config = propagation_config["network_scan"]
addresses_to_scan = self._compile_scan_target_list(target_config)
addresses_to_scan = self._compile_scan_target_list(scan_config.targets)
self._ip_scanner.scan(addresses_to_scan, scan_config, self._process_scan_results, stop)
logger.info("Finished network scan")
def _compile_scan_target_list(self, target_config: Dict) -> List[NetworkAddress]:
ranges_to_scan = target_config["subnet_scan_list"]
inaccessible_subnets = target_config["inaccessible_subnets"]
blocklisted_ips = target_config["blocked_ips"]
enable_local_network_scan = target_config["local_network_scan"]
def _compile_scan_target_list(
self, target_config: ScanTargetConfiguration
) -> List[NetworkAddress]:
ranges_to_scan = target_config.subnets
inaccessible_subnets = target_config.inaccessible_subnets
blocklisted_ips = target_config.blocked_ips
enable_local_network_scan = target_config.local_network_scan
return compile_scan_target_list(
self._local_network_interfaces,
@ -134,14 +142,14 @@ class Propagator:
def _exploit_hosts(
self,
propagation_config: Dict,
propagation_config: PropagationConfiguration,
current_depth: int,
network_scan_completed: Event,
stop: Event,
):
logger.info("Exploiting victims")
exploiter_config = propagation_config["exploiters"]
exploiter_config = propagation_config.exploitation
self._exploiter.exploit_hosts(
exploiter_config,
self._hosts_to_exploit,

View File

@ -1,5 +1,7 @@
from typing import Optional
from common import OperatingSystems
class VictimHost(object):
def __init__(self, ip_addr: str, domain_name: str = ""):
@ -14,6 +16,9 @@ class VictimHost(object):
def as_dict(self):
return self.__dict__
def is_windows(self) -> bool:
return OperatingSystems.WINDOWS == self.os["type"]
def __hash__(self):
return hash(self.ip_addr)

View File

@ -78,7 +78,7 @@ from infection_monkey.utils.monkey_dir import (
remove_monkey_dir,
)
from infection_monkey.utils.monkey_log_path import get_agent_log_path
from infection_monkey.utils.propagation import should_propagate
from infection_monkey.utils.propagation import maximum_depth_reached
from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers
logger = logging.getLogger(__name__)
@ -152,7 +152,6 @@ class InfectionMonkey:
raise Exception(f"Monkey couldn't find server with {self._opts.tunnel} default tunnel.")
self._control_client.wakeup(parent=self._opts.parent)
self._control_client.load_control_config()
def _current_server_is_set(self) -> bool:
if self._control_client.find_server(default_tunnel=self._opts.tunnel):
@ -168,11 +167,17 @@ class InfectionMonkey:
if firewall.is_enabled():
firewall.add_firewall_rule()
self._monkey_inbound_tunnel = self._control_client.create_control_tunnel()
config = ControlChannel(
control_channel = ControlChannel(
self._control_client.server_address, GUID, self._control_client.proxies
).get_config()
if self._monkey_inbound_tunnel and should_propagate(config, self._current_depth):
)
config = control_channel.get_config()
self._monkey_inbound_tunnel = self._control_client.create_control_tunnel(
config.keep_tunnel_open_time
)
if self._monkey_inbound_tunnel and maximum_depth_reached(
config.propagation.maximum_depth, self._current_depth
):
self._inbound_tunnel_opened = True
self._monkey_inbound_tunnel.start()

View File

@ -5,6 +5,7 @@ import re
import subprocess
import sys
from common import OperatingSystems
from infection_monkey.i_puppet import PingScanData
from infection_monkey.utils.environment import is_windows_os
@ -79,9 +80,9 @@ def _process_ping_command_output(ping_command_output: str) -> PingScanData:
operating_system = None
if ttl <= LINUX_TTL:
operating_system = "linux"
operating_system = OperatingSystems.LINUX
else: # as far we we know, could also be OSX/BSD, but lets handle that when it comes up.
operating_system = "windows"
operating_system = OperatingSystems.WINDOWS
return PingScanData(True, operating_system)

View File

@ -5,6 +5,7 @@ from typing import Dict
from odict import odict
from common import OperatingSystems
from infection_monkey.i_puppet import (
FingerprintData,
IFingerprinter,
@ -193,9 +194,9 @@ class SMBFingerprinter(IFingerprinter):
logger.debug(f'os_version: "{os_version}", service_client: "{service_client}"')
if os_version.lower() != "unix":
os_type = "windows"
os_type = OperatingSystems.WINDOWS
else:
os_type = "linux"
os_type = OperatingSystems.LINUX
smb_service["name"] = service_client

View File

@ -1,6 +1,7 @@
import re
from typing import Dict, Optional, Tuple
from common import OperatingSystems
from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData
SSH_REGEX = r"SSH-\d\.\d-OpenSSH"
@ -40,6 +41,6 @@ class SSHFingerprinter(IFingerprinter):
for dist in LINUX_DIST_SSH:
if banner.lower().find(dist) != -1:
os_version = banner.split(" ").pop().strip()
os = "linux"
os = OperatingSystems.LINUX
return os, os_version

View File

@ -16,7 +16,7 @@ class ClearCommandHistory(PBA):
super().__init__(telemetry_messenger, name=POST_BREACH_CLEAR_CMD_HISTORY)
def run(self, options: Dict) -> Iterable[PostBreachData]:
results = [pba.run() for pba in self.clear_command_history_pba_list()]
results = [pba.run(options) for pba in self.clear_command_history_pba_list()]
if results:
# `self.command` is empty here
self.pba_data.append(PostBreachData(self.name, self.command, results))
@ -53,7 +53,7 @@ class ClearCommandHistory(PBA):
linux_cmd=linux_cmds,
)
def run(self) -> Tuple[str, bool]:
def run(self, options: Dict) -> Tuple[str, bool]:
if self.command:
try:
output = subprocess.check_output( # noqa: DUO116

View File

@ -4,6 +4,7 @@ import logging
from infection_monkey.control import ControlClient
from infection_monkey.telemetry.i_telem import ITelem
from infection_monkey.telemetry.telem_encoder import TelemetryJSONEncoder
logger = logging.getLogger(__name__)
LOGGED_DATA_LENGTH = 300 # How many characters of telemetry data will be logged
@ -39,7 +40,7 @@ class BaseTelem(ITelem, metaclass=abc.ABCMeta):
@property
def json_encoder(self):
return json.JSONEncoder
return TelemetryJSONEncoder
def _log_telem_sending(self, serialized_data: str, log_data=True):
logger.debug(f"Sending {self.telem_category} telemetry.")

View File

@ -0,0 +1,10 @@
import json
from common import OperatingSystems
class TelemetryJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, OperatingSystems):
return obj.name
return json.JSONEncoder.default(self, obj)

View File

@ -62,7 +62,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
f.close()
def send_head(self):
if self.path != "/" + urllib.parse.quote(self.victim_os):
if self.path != "/" + urllib.parse.quote(self.victim_os.value):
self.send_error(500, "")
return None, 0, 0
try:

View File

@ -1,2 +1,2 @@
def should_propagate(config: dict, current_depth: int) -> bool:
return config["config"]["depth"] > current_depth
def maximum_depth_reached(maximum_depth: int, current_depth: int) -> bool:
return maximum_depth > current_depth

View File

@ -28,6 +28,8 @@ cffi = "*" # Without explicit install: ModuleNotFoundError: No module named '_c
pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows
pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified
pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows
marshmallow = "*"
marshmallow-enum = "*"
[dev-packages]
virtualenv = ">=20.0.26"

View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "945e5bfe369347efb543a6c8f0ec2bb90df87284d0a65455944479ec2f78a5fa"
"sha256": "04efa1f593acdfcdc48b7089108921a46421acbacec80d8a664ec674b221dd4b"
},
"pipfile-spec": 6,
"requires": {
@ -72,11 +72,11 @@
},
"certifi": {
"hashes": [
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7",
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
],
"markers": "python_version >= '3.6'",
"version": "==2022.5.18.1"
"version": "==2022.6.15"
},
"cffi": {
"hashes": [
@ -139,7 +139,7 @@
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12"
},
"click": {
@ -152,11 +152,11 @@
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
"sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da",
"sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"
],
"markers": "platform_system == 'Windows'",
"version": "==0.4.4"
"version": "==0.4.5"
},
"cryptography": {
"hashes": [
@ -338,7 +338,7 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"markers": "python_full_version >= '3.5.0'",
"version": "==3.3"
},
"importlib-metadata": {
@ -435,6 +435,22 @@
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
},
"marshmallow": {
"hashes": [
"sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1",
"sha256:a762c1d8b2bcb0e5c8e964850d03f9f3bffd6a12b626f3c14b9d6b1841999af5"
],
"index": "pypi",
"version": "==3.16.0"
},
"marshmallow-enum": {
"hashes": [
"sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58",
"sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"
],
"index": "pypi",
"version": "==1.5.1"
},
"mongoengine": {
"hashes": [
"sha256:6e127f45f71c2bc5e72461ec297a0c20f04c3ee0bf6dd869e336226e325db6ef",
@ -479,6 +495,14 @@
"index": "pypi",
"version": "==0.11.0"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pefile": {
"hashes": [
"sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"
@ -688,6 +712,14 @@
],
"version": "==3.12.3"
},
"pyparsing": {
"hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.9"
},
"pyrsistent": {
"hashes": [
"sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c",
@ -762,11 +794,11 @@
},
"requests": {
"hashes": [
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f",
"sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"
],
"index": "pypi",
"version": "==2.27.1"
"version": "==2.28.0"
},
"ring": {
"hashes": [
@ -785,11 +817,11 @@
},
"setuptools": {
"hashes": [
"sha256:68e45d17c9281ba25dc0104eadd2647172b3472d9e01f911efa57965e8d51a36",
"sha256:a43bdedf853c670e5fed28e5623403bad2f73cf02f9a2774e91def6bda8265a7"
"sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77",
"sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91"
],
"markers": "python_version >= '3.7'",
"version": "==62.3.2"
"version": "==62.4.0"
},
"six": {
"hashes": [
@ -928,11 +960,11 @@
},
"babel": {
"hashes": [
"sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2",
"sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13"
"sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51",
"sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"
],
"markers": "python_version >= '3.6'",
"version": "==2.10.1"
"version": "==2.10.3"
},
"black": {
"hashes": [
@ -965,18 +997,18 @@
},
"certifi": {
"hashes": [
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7",
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
],
"markers": "python_version >= '3.6'",
"version": "==2022.5.18.1"
"version": "==2022.6.15"
},
"charset-normalizer": {
"hashes": [
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12"
},
"click": {
@ -989,11 +1021,11 @@
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
"sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da",
"sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"
],
"markers": "platform_system == 'Windows'",
"version": "==0.4.4"
"version": "==0.4.5"
},
"coverage": {
"hashes": [
@ -1085,7 +1117,7 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"markers": "python_full_version >= '3.5.0'",
"version": "==3.3"
},
"imagesize": {
@ -1291,11 +1323,11 @@
},
"requests": {
"hashes": [
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f",
"sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"
],
"index": "pypi",
"version": "==2.27.1"
"version": "==2.28.0"
},
"requests-mock": {
"hashes": [
@ -1313,11 +1345,11 @@
},
"setuptools": {
"hashes": [
"sha256:68e45d17c9281ba25dc0104eadd2647172b3472d9e01f911efa57965e8d51a36",
"sha256:a43bdedf853c670e5fed28e5623403bad2f73cf02f9a2774e91def6bda8265a7"
"sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77",
"sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91"
],
"markers": "python_version >= '3.7'",
"version": "==62.3.2"
"version": "==62.4.0"
},
"six": {
"hashes": [

View File

@ -12,6 +12,7 @@ from common import DIContainer
from monkey_island.cc.database import database, mongo
from monkey_island.cc.resources import AgentBinaries, RemoteRun
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.agent_configuration import AgentConfiguration
from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents
from monkey_island.cc.resources.attack.attack_report import AttackReport
from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt
@ -155,6 +156,7 @@ def init_api_resources(api: FlaskDIWrapper):
api.add_resource(IslandConfiguration)
api.add_resource(ConfigurationExport)
api.add_resource(ConfigurationImport)
api.add_resource(AgentConfiguration)
api.add_resource(AgentBinaries)
api.add_resource(NetMap)
api.add_resource(Edge)

View File

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

View File

@ -1,6 +1,6 @@
from typing import BinaryIO
from . import AgentRetrievalError, FileRetrievalError, IAgentBinaryRepository, IFileRepository
from . import IAgentBinaryRepository, IFileRepository, RetrievalError
LINUX_AGENT_FILE_NAME = "monkey-linux-64"
WINDOWS_AGENT_FILE_NAME = "monkey-windows-64.exe"
@ -20,8 +20,8 @@ class AgentBinaryRepository(IAgentBinaryRepository):
try:
agent_binary = self._file_repository.open_file(filename)
return agent_binary
except FileRetrievalError as err:
raise AgentRetrievalError(
except Exception as err:
raise RetrievalError(
f"An error occurred while retrieving the {filename}"
f" agent binary from {self._file_repository}: {err}"
)

View File

@ -0,0 +1,22 @@
class RemovalError(RuntimeError):
"""
Raised when a repository encounters an error while attempting to remove data.
"""
pass
class RetrievalError(RuntimeError):
"""
Raised when a repository encounters an error while attempting to retrieve data.
"""
pass
class StorageError(RuntimeError):
"""
Raised when a repository encounters an error while attempting to store data.
"""
pass

View File

@ -0,0 +1,37 @@
import io
from common.configuration import AgentConfiguration
from monkey_island.cc import repository
from monkey_island.cc.repository import (
IAgentConfigurationRepository,
IFileRepository,
RetrievalError,
)
AGENT_CONFIGURATION_FILE_NAME = "agent_configuration.json"
class FileAgentConfigurationRepository(IAgentConfigurationRepository):
def __init__(
self, default_agent_configuration: AgentConfiguration, file_repository: IFileRepository
):
self._default_agent_configuration = default_agent_configuration
self._file_repository = file_repository
def get_configuration(self) -> AgentConfiguration:
try:
with self._file_repository.open_file(AGENT_CONFIGURATION_FILE_NAME) as f:
configuration_json = f.read().decode()
return AgentConfiguration.from_json(configuration_json)
except repository.FileNotFoundError:
return self._default_agent_configuration
except Exception as err:
raise RetrievalError(f"Error retrieving the agent configuration: {err}")
def store_configuration(self, agent_configuration: AgentConfiguration):
configuration_json = AgentConfiguration.to_json(agent_configuration)
self._file_repository.save_file(
AGENT_CONFIGURATION_FILE_NAME, io.BytesIO(configuration_json.encode())
)

View File

@ -1,2 +1,2 @@
from .i_file_repository import IFileRepository, FileRetrievalError
from .i_file_repository import IFileRepository, FileNotFoundError
from .local_storage_file_repository import LocalStorageFileRepository

View File

@ -1,8 +1,10 @@
import abc
from typing import BinaryIO
from monkey_island.cc.repository import RetrievalError
class FileRetrievalError(RuntimeError):
class FileNotFoundError(RetrievalError):
pass
@ -18,6 +20,7 @@ class IFileRepository(metaclass=abc.ABCMeta):
:param unsafe_file_name: An unsanitized file name that will identify the file
:param file_contents: The data to be stored in the file
:raises StorageError: If an error was encountered while attempting to store the file
"""
pass
@ -28,7 +31,8 @@ class IFileRepository(metaclass=abc.ABCMeta):
:param unsafe_file_name: An unsanitized file name that identifies the file to be opened
:return: A file-like object providing access to the file's contents
:raises FileRetrievalError: if the file cannot be opened
:raises FileNotFoundError: if the file does not exist
:raises RetrievalError: if the file exists but cannot be retrieved
"""
pass
@ -41,6 +45,7 @@ class IFileRepository(metaclass=abc.ABCMeta):
idempotent and will succeed if the file to be deleted does not exist.
:param unsafe_file_name: An unsanitized file name that identifies the file to be deleted
:raises RemovalError: If an error was encountered while attempting to remove a file
"""
pass
@ -48,5 +53,7 @@ class IFileRepository(metaclass=abc.ABCMeta):
def delete_all_files(self):
"""
Delete all files that have been stored using this service.
:raises RemovalError: If an error was encountered while attempting to remove a file
"""
pass

View File

@ -4,9 +4,10 @@ from pathlib import Path
from typing import BinaryIO
from common.utils.file_utils import get_all_regular_files_in_directory
from monkey_island.cc.repository import RemovalError, RetrievalError, StorageError
from monkey_island.cc.server_utils.file_utils import create_secure_directory
from . import FileRetrievalError, IFileRepository
from . import IFileRepository, i_file_repository
logger = logging.getLogger(__name__)
@ -31,34 +32,42 @@ class LocalStorageFileRepository(IFileRepository):
self._storage_directory = storage_directory
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
try:
safe_file_path = self._get_safe_file_path(unsafe_file_name)
logger.debug(f"Saving file contents to {safe_file_path}")
with open(safe_file_path, "wb") as dest:
shutil.copyfileobj(file_contents, dest)
except Exception as err:
raise StorageError(f"Error while attempting to store {unsafe_file_name}: {err}")
def open_file(self, unsafe_file_name: str) -> BinaryIO:
try:
safe_file_path = self._get_safe_file_path(unsafe_file_name)
try:
logger.debug(f"Opening {safe_file_path}")
return open(safe_file_path, "rb")
except OSError as err:
# TODO: The interface should make a destinction between file not found and an error when
# retrieving a file that should exist. The built-in `FileNotFoundError` is not
# sufficient because it inherits from `OSError` and the interface does not
# guarantee that the file is stored on the local file system.
raise FileRetrievalError(f"Failed to retrieve file {safe_file_path}: {err}") from err
except FileNotFoundError as err:
# Wrap Python's FileNotFound error, which is-an OSError, in repository.FileNotFoundError
raise i_file_repository.FileNotFoundError(
f'The requested file "{unsafe_file_name}" does not exist: {err}'
)
except Exception as err:
raise RetrievalError(
f'Error retrieving file "{unsafe_file_name}" from the repository: {err}'
)
def delete_file(self, unsafe_file_name: str):
try:
safe_file_path = self._get_safe_file_path(unsafe_file_name)
try:
logger.debug(f"Deleting {safe_file_path}")
safe_file_path.unlink()
except FileNotFoundError:
# This method is idempotent.
pass
except Exception as err:
raise RemovalError(f"Error while attempting to remove {unsafe_file_name}: {err}")
def _get_safe_file_path(self, unsafe_file_name: str):
# Remove any path information from the file name.
@ -67,12 +76,17 @@ class LocalStorageFileRepository(IFileRepository):
# This is a paranoid check to avoid directory traversal attacks.
if self._storage_directory.resolve() not in safe_file_path.parents:
raise ValueError(f"The file named {unsafe_file_name} can not be safely retrieved")
raise ValueError(
f'The file named "{unsafe_file_name}" cannot be safely retrieved or written'
)
logger.debug(f"Untrusted file name {unsafe_file_name} sanitized: {safe_file_path}")
return safe_file_path
def delete_all_files(self):
try:
for file in get_all_regular_files_in_directory(self._storage_directory):
logger.debug(f"Deleting {file}")
file.unlink()
except Exception as err:
raise RemovalError(f"Error while attempting to clear the repository: {err}")

View File

@ -2,10 +2,6 @@ import abc
from typing import BinaryIO
class AgentRetrievalError(IOError):
pass
class IAgentBinaryRepository(metaclass=abc.ABCMeta):
"""
A repository that retrieves the agent binaries
@ -17,6 +13,7 @@ class IAgentBinaryRepository(metaclass=abc.ABCMeta):
Retrieve linux agent binary
:return: A file-like object that represents the linux agent binary
:raises RetrievalError: If the agent binary could not be retrieved
"""
@abc.abstractmethod
@ -25,4 +22,5 @@ class IAgentBinaryRepository(metaclass=abc.ABCMeta):
Retrieve windows agent binary
:return: A file-like object that represents the windows agent binary
:raises RetrievalError: If the agent binary could not be retrieved
"""

View File

@ -0,0 +1,30 @@
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 the repository is empty
:raises RetrievalError: If the configuration could 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
:raises StorageError: If the configuration could not be stored
"""
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

@ -2,7 +2,7 @@ import logging
from flask import make_response, send_file
from monkey_island.cc.repository import AgentRetrievalError, IAgentBinaryRepository
from monkey_island.cc.repository import IAgentBinaryRepository, RetrievalError
from monkey_island.cc.resources.AbstractResource import AbstractResource
logger = logging.getLogger(__name__)
@ -31,10 +31,10 @@ class AgentBinaries(AbstractResource):
file = agent_binaries[os]()
return send_file(file, mimetype="application/octet-stream")
except AgentRetrievalError as err:
logger.error(err)
return make_response({"error": str(err)}, 500)
except KeyError as err:
error_msg = f'No Agents are available for unsupported operating system "{os}": {err}'
logger.error(error_msg)
return make_response({"error": error_msg}, 404)
except RetrievalError as err:
logger.error(err)
return make_response({"error": str(err)}, 500)

View File

@ -0,0 +1,35 @@
import json
from flask import make_response, request
from common.configuration.agent_configuration import AgentConfiguration as AgentConfigurationObject
from common.configuration.agent_configuration import InvalidConfigurationError
from monkey_island.cc.repository import IAgentConfigurationRepository
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.request_authentication import jwt_required
class AgentConfiguration(AbstractResource):
urls = ["/api/agent-configuration"]
def __init__(self, agent_configuration_repository: IAgentConfigurationRepository):
self._agent_configuration_repository = agent_configuration_repository
@jwt_required
def get(self):
configuration = self._agent_configuration_repository.get_configuration()
configuration_json = AgentConfigurationObject.to_json(configuration)
return make_response(configuration_json, 200)
@jwt_required
def post(self):
try:
configuration_object = AgentConfigurationObject.from_json(request.data)
self._agent_configuration_repository.store_configuration(configuration_object)
return make_response({}, 200)
except (InvalidConfigurationError, json.JSONDecodeError) as err:
return make_response(
{"error": f"Invalid configuration supplied: {err}"},
400,
)

View File

@ -2,7 +2,8 @@ import logging
from flask import make_response, send_file
from monkey_island.cc.repository import FileRetrievalError, IFileRepository
from monkey_island.cc import repository
from monkey_island.cc.repository import IFileRepository
from monkey_island.cc.resources.AbstractResource import AbstractResource
logger = logging.getLogger(__file__)
@ -24,7 +25,6 @@ class PBAFileDownload(AbstractResource):
# `send_file()` handles the closing of the open file.
return send_file(file, mimetype="application/octet-stream")
except FileRetrievalError as err:
error_msg = f"Failed to open file {filename}: {err}"
logger.error(error_msg)
return make_response({"error": error_msg}, 404)
except repository.FileNotFoundError as err:
logger.error(str(err))
return make_response({"error": str(err)}, 404)

View File

@ -5,7 +5,8 @@ from flask import Response, make_response, request, send_file
from werkzeug.utils import secure_filename as sanitize_filename
from common.config_value_paths import PBA_LINUX_FILENAME_PATH, PBA_WINDOWS_FILENAME_PATH
from monkey_island.cc.repository import FileRetrievalError, IFileRepository
from monkey_island.cc import repository
from monkey_island.cc.repository import IFileRepository
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.request_authentication import jwt_required
from monkey_island.cc.services.config import ConfigService
@ -57,10 +58,9 @@ class FileUpload(AbstractResource):
# `send_file()` handles the closing of the open file.
return send_file(file, mimetype="application/octet-stream")
except FileRetrievalError as err:
error_msg = f"Failed to open file {filename}: {err}"
logger.error(error_msg)
return make_response({"error": error_msg}, 404)
except repository.FileNotFoundError as err:
logger.error(str(err))
return make_response({"error": str(err)}, 404)
@jwt_required
def post(self, target_os):

View File

@ -3,7 +3,6 @@ import copy
import functools
import logging
import re
from itertools import chain
from typing import Any, Dict, List
from jsonschema import Draft4Validator, validators
@ -180,6 +179,7 @@ class ConfigService:
should_encrypt=True,
)
@staticmethod
def _filter_none_values(data):
if isinstance(data, dict):
return {
@ -357,6 +357,7 @@ class ConfigService:
ConfigService._format_payloads_from_flat_config(config)
ConfigService._format_pbas_from_flat_config(config)
ConfigService._format_propagation_from_flat_config(config)
ConfigService._format_credential_collectors(config)
# Ok, I'll admit this is just sort of jammed in here. But this code is going away very soon.
del config["HTTP_PORTS"]
@ -376,9 +377,18 @@ class ConfigService:
for field in fields_to_remove:
config.pop(field, None)
@staticmethod
def _format_credential_collectors(config: Dict):
collectors = [
{"name": collector, "options": {}} for collector in config["credential_collectors"]
]
config["credential_collectors"] = collectors
@staticmethod
def _format_payloads_from_flat_config(config: Dict):
config.setdefault("payloads", {})["ransomware"] = config["ransomware"]
config.setdefault("payloads", []).append(
{"name": "ransomware", "options": config["ransomware"]}
)
config.pop("ransomware", None)
@staticmethod
@ -388,9 +398,9 @@ class ConfigService:
flat_windows_command_field = "custom_PBA_windows_cmd"
flat_windows_filename_field = "PBA_windows_filename"
formatted_pbas_config = {}
for pba in config.get("post_breach_actions", []):
formatted_pbas_config[pba] = {}
formatted_pbas_config = [
{"name": pba, "options": {}} for pba in config.get("post_breach_actions", [])
]
config["custom_pbas"] = {
"linux_command": config.get(flat_linux_command_field, ""),
@ -408,24 +418,24 @@ class ConfigService:
@staticmethod
def _format_propagation_from_flat_config(config: Dict):
formatted_propagation_config = {"network_scan": {}, "targets": {}}
formatted_propagation_config = {"network_scan": {}, "maximum_depth": {}, "exploitation": {}}
formatted_propagation_config[
"network_scan"
] = ConfigService._format_network_scan_from_flat_config(config)
formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config(
config
)
formatted_propagation_config[
"exploiters"
"exploitation"
] = ConfigService._format_exploiters_from_flat_config(config)
formatted_propagation_config["maximum_depth"] = config["depth"]
del config["depth"]
config["propagation"] = formatted_propagation_config
@staticmethod
def _format_network_scan_from_flat_config(config: Dict) -> Dict[str, Any]:
formatted_network_scan_config = {"tcp": {}, "icmp": {}, "fingerprinters": []}
formatted_network_scan_config = {"tcp": {}, "icmp": {}, "fingerprinters": [], "targets": {}}
formatted_network_scan_config["tcp"] = ConfigService._format_tcp_scan_from_flat_config(
config
@ -437,6 +447,10 @@ class ConfigService:
"fingerprinters"
] = ConfigService._format_fingerprinters_from_flat_config(config)
formatted_network_scan_config["targets"] = ConfigService._format_targets_from_flat_config(
config
)
return formatted_network_scan_config
@staticmethod
@ -447,7 +461,7 @@ class ConfigService:
formatted_tcp_scan_config = {}
formatted_tcp_scan_config["timeout_ms"] = config[flat_tcp_timeout_field]
formatted_tcp_scan_config["timeout"] = config[flat_tcp_timeout_field] / 1000
ports = ConfigService._union_tcp_and_http_ports(
config[flat_tcp_ports_field], config[flat_http_ports_field]
@ -471,7 +485,7 @@ class ConfigService:
flat_ping_timeout_field = "ping_scan_timeout"
formatted_icmp_scan_config = {}
formatted_icmp_scan_config["timeout_ms"] = config[flat_ping_timeout_field]
formatted_icmp_scan_config["timeout"] = config[flat_ping_timeout_field] / 1000
config.pop(flat_ping_timeout_field, None)
@ -519,9 +533,7 @@ class ConfigService:
formatted_scan_targets_config[flat_local_network_scan_field] = config[
flat_local_network_scan_field
]
formatted_scan_targets_config[flat_subnet_scan_list_field] = config[
flat_subnet_scan_list_field
]
formatted_scan_targets_config["subnets"] = config[flat_subnet_scan_list_field]
config.pop(flat_blocked_ips_field, None)
config.pop(flat_inaccessible_subnets_field, None)
@ -567,7 +579,7 @@ class ConfigService:
formatted_exploiters_config = ConfigService._add_smb_download_timeout_to_exploiters(
formatted_exploiters_config
)
return ConfigService._add_supported_os_to_exploiters(formatted_exploiters_config)
return formatted_exploiters_config
@staticmethod
def _add_smb_download_timeout_to_exploiters(
@ -580,23 +592,3 @@ class ConfigService:
exploiter["options"]["smb_download_timeout"] = SMB_DOWNLOAD_TIMEOUT
return new_config
@staticmethod
def _add_supported_os_to_exploiters(
formatted_config: Dict,
) -> Dict[str, List[Dict[str, Any]]]:
supported_os = {
"HadoopExploiter": ["linux", "windows"],
"Log4ShellExploiter": ["linux", "windows"],
"MSSQLExploiter": ["windows"],
"PowerShellExploiter": ["windows"],
"SSHExploiter": ["linux"],
"SmbExploiter": ["windows"],
"WmiExploiter": ["windows"],
"ZerologonExploiter": ["windows"],
}
new_config = copy.deepcopy(formatted_config)
for exploiter in chain(new_config["brute_force"], new_config["vulnerability"]):
exploiter["supported_os"] = supported_os.get(exploiter["name"], [])
return new_config

View File

@ -3,13 +3,16 @@ from pathlib import Path
from common import DIContainer
from common.aws import AWSInstance
from common.configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfiguration
from common.utils.file_utils import get_binary_io_sha256_hash
from monkey_island.cc.repository import (
AgentBinaryRepository,
AgentRetrievalError,
FileAgentConfigurationRepository,
IAgentBinaryRepository,
IAgentConfigurationRepository,
IFileRepository,
LocalStorageFileRepository,
RetrievalError,
)
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.services import AWSService
@ -28,14 +31,20 @@ def initialize_services(data_dir: Path) -> DIContainer:
container = DIContainer()
container.register_convention(Path, "data_dir", data_dir)
container.register_convention(
AgentConfiguration, "default_agent_configuration", DEFAULT_AGENT_CONFIGURATION
)
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))
@ -71,7 +80,7 @@ def _log_agent_binary_hashes(agent_binary_repository: IAgentBinaryRepository):
agent_binary = get_agent_binary()
binary_sha256_hash = get_binary_io_sha256_hash(agent_binary)
agent_hashes[os] = binary_sha256_hash
except AgentRetrievalError as err:
except RetrievalError as err:
logger.error(f"No agent available for {os}: {err}")
for os, binary_sha256_hash in agent_hashes.items():

View File

@ -1,4 +1,5 @@
from datetime import datetime
from enum import Enum
import bson
from bson.json_util import dumps
@ -11,19 +12,27 @@ def normalize_obj(obj):
del obj["_id"]
for key, value in list(obj.items()):
if isinstance(value, bson.objectid.ObjectId):
obj[key] = str(value)
if isinstance(value, datetime):
obj[key] = str(value)
if isinstance(value, dict):
obj[key] = normalize_obj(value)
if isinstance(value, list):
for i in range(0, len(value)):
if isinstance(value[i], dict):
value[i] = normalize_obj(value[i])
obj[key][i] = _normalize_value(value[i])
else:
obj[key] = _normalize_value(value)
return obj
def _normalize_value(value):
if type(value) == dict:
return normalize_obj(value)
if isinstance(value, bson.objectid.ObjectId):
return str(value)
if isinstance(value, datetime):
return str(value)
if issubclass(type(value), Enum):
return value.name
else:
return value
def output_json(obj, code, headers=None):
obj = normalize_obj(obj)
resp = make_response(dumps(obj), code)

View File

@ -5,7 +5,7 @@ import subprocess
from pathlib import Path
from shutil import copyfileobj
from monkey_island.cc.repository import AgentRetrievalError, IAgentBinaryRepository
from monkey_island.cc.repository import IAgentBinaryRepository, RetrievalError
from monkey_island.cc.server_utils.consts import ISLAND_PORT
from monkey_island.cc.services.utils.network_utils import local_ip_addresses
@ -29,7 +29,7 @@ class LocalMonkeyRunService:
}
agent_binary = agents[platform.system().lower()]()
except AgentRetrievalError as err:
except RetrievalError as err:
logger.error(
f"No Agent can be retrieved for the specified operating system"
f'"{operating_system}"'

View File

@ -0,0 +1,73 @@
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": {}},
{
"name": "ex2",
"options": {"smb_download_timeout": 10},
},
]
VULNERABILITY = [
{
"name": "ex3",
"options": {"smb_download_timeout": 10},
},
]
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

@ -1,107 +0,0 @@
{
"config": {
"propagation": {
"network_scan": {
"tcp": {
"timeout_ms": 3000,
"ports": [
22,
2222,
445,
135,
3389,
80,
8080,
443,
8008,
3306,
7001,
8088,
9200
]
},
"icmp": {
"timeout_ms": 1000
},
"fingerprinters": [
"SMBFinger",
"SSHFinger",
"HTTPFinger",
"MySQLFinger",
"MSSQLFinger",
"ElasticFinger"
]
},
"targets": {
"blocked_ips": ["192.168.1.1", "192.168.1.100"],
"inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"],
"local_network_scan": true,
"subnet_scan_list": [
"192.168.1.50",
"192.168.56.0/24",
"10.0.33.0/30",
"10.0.0.1",
"10.0.0.2"
]
},
"exploiters": {
"options": {},
"brute_force": [
{"name": "MSSQLExploiter", "supported_os": ["windows"], "options": {}},
{"name": "PowerShellExploiter", "supported_os": ["windows"], "options": {}},
{"name": "SmbExploiter", "supported_os": ["windows"], "options": {}},
{"name": "SSHExploiter", "supported_os": ["linux"], "options": {}},
{"name": "WmiExploiter", "supported_os": ["windows"], "options": {}}
],
"vulnerability": [
{"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}},
{"name": "ShellShockExploiter", "supported_os": ["linux"], "options": {}},
{"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}
]
}
},
"PBA_linux_filename": "",
"PBA_windows_filename": "",
"custom_pbas": {
"linux_command": "",
"windows_command": ""
},
"depth": 2,
"exploit_lm_hash_list": ["DEADBEEF", "FACADE"],
"exploit_ntlm_hash_list": ["BEADED", "ACCEDE", "DECADE"],
"exploit_password_list": ["p1", "p2", "p3"],
"exploit_ssh_keys": "hidden",
"exploit_user_list": ["u1", "u2", "u3"],
"exploiter_classes": [],
"max_depth": 2,
"post_breach_actions": {
"CommunicateAsBackdoorUser": {},
"ModifyShellStartupFiles": {},
"HiddenFiles": {},
"TrapCommand": {},
"ChangeSetuidSetgid": {},
"ScheduleJobs": {},
"Timestomping": {},
"AccountDiscovery": {},
"Custom": {
"linux_command": "chmod u+x my_exec && ./my_exec",
"windows_cmd": "powershell test_driver.ps1",
"linux_filename": "my_exec",
"windows_filename": "test_driver.ps1"
}
},
"payloads": {
"ransomware": {
"encryption": {
"directories": {"linux_target_dir": "", "windows_target_dir": ""},
"enabled": true
},
"other_behaviors": {"readme": true}
}
},
"credential_collectors": [
"MimikatzCollector",
"SSHCollector"
]
}
}

View File

@ -9,7 +9,6 @@
],
"PBA_linux_filename": "test.sh",
"PBA_windows_filename": "test.ps1",
"alive": true,
"blocked_ips": ["192.168.1.1", "192.168.1.100"],
"custom_PBA_linux_cmd": "bash test.sh",
"custom_PBA_windows_cmd": "powershell test.ps1",
@ -27,6 +26,7 @@
"private_key": "my_private_key"
}
],
"credential_collectors": ["MimikatzCollector", "SSHCollector"],
"exploit_user_list": [
"Administrator",
"root",
@ -53,7 +53,6 @@
"inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"],
"keep_tunnel_open_time": 60,
"local_network_scan": true,
"max_depth": null,
"ping_scan_timeout": 1000,
"post_breach_actions": [
"CommunicateAsBackdoorUser",
@ -75,9 +74,6 @@
}
},
"subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"],
"system_info_collector_classes": [
"MimikatzCollector"
],
"tcp_scan_timeout": 3000,
"tcp_target_ports": [
22,

View File

@ -0,0 +1,4 @@
from .single_file_repository import SingleFileRepository
from .mock_file_repository import MockFileRepository, FILE_CONTENTS, FILE_NAME
from .open_error_file_repository import OpenErrorFileRepository
from .in_memory_agent_configuration_repository import InMemoryAgentConfigurationRepository

View File

@ -0,0 +1,15 @@
from tests.common.example_agent_configuration import AGENT_CONFIGURATION
from common.configuration.agent_configuration import AgentConfiguration
from monkey_island.cc.repository import IAgentConfigurationRepository
class InMemoryAgentConfigurationRepository(IAgentConfigurationRepository):
def __init__(self):
self._configuration = AgentConfiguration.from_mapping(AGENT_CONFIGURATION)
def get_configuration(self):
return self._configuration
def store_configuration(self, agent_configuration):
self._configuration = agent_configuration

View File

@ -0,0 +1,28 @@
import io
from typing import BinaryIO
from monkey_island.cc import repository
from monkey_island.cc.repository import IFileRepository
FILE_NAME = "test_file"
FILE_CONTENTS = b"HelloWorld!"
class MockFileRepository(IFileRepository):
def __init__(self):
self._file = io.BytesIO(FILE_CONTENTS)
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
pass
def open_file(self, unsafe_file_name: str) -> BinaryIO:
if unsafe_file_name != FILE_NAME:
raise repository.FileNotFoundError()
return self._file
def delete_file(self, unsafe_file_name: str):
pass
def delete_all_files(self):
pass

View File

@ -0,0 +1,10 @@
from typing import BinaryIO
from monkey_island.cc.repository import RetrievalError
from . import MockFileRepository
class OpenErrorFileRepository(MockFileRepository):
def open_file(self, unsafe_file_name: str) -> BinaryIO:
raise RetrievalError("Error retrieving file")

View File

@ -0,0 +1,24 @@
import io
from typing import BinaryIO
from monkey_island.cc import repository
from monkey_island.cc.repository import 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 repository.FileNotFoundError()
return self._file
def delete_file(self, unsafe_file_name: str):
self._file = None
def delete_all_files(self):
self.delete_file("")

View File

@ -0,0 +1,219 @@
import json
from copy import deepcopy
import pytest
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.configuration import AgentConfiguration, InvalidConfigurationError
from common.configuration.agent_configuration import AgentConfigurationSchema
from common.configuration.agent_sub_configuration_schemas import (
CustomPBAConfigurationSchema,
ExploitationConfigurationSchema,
ExploitationOptionsConfigurationSchema,
ICMPScanConfigurationSchema,
NetworkScanConfigurationSchema,
PluginConfigurationSchema,
PropagationConfigurationSchema,
ScanTargetConfigurationSchema,
TCPScanConfigurationSchema,
)
from common.configuration.agent_sub_configurations import (
CustomPBAConfiguration,
ExploitationConfiguration,
NetworkScanConfiguration,
PluginConfiguration,
PropagationConfiguration,
)
def test_build_plugin_configuration():
schema = PluginConfigurationSchema()
config = schema.load(PLUGIN_CONFIGURATION)
assert config.name == PLUGIN_NAME
assert config.options == PLUGIN_OPTIONS
def test_custom_pba_configuration_schema():
schema = CustomPBAConfigurationSchema()
config = schema.load(CUSTOM_PBA_CONFIGURATION)
assert config.linux_command == LINUX_COMMAND
assert config.linux_filename == LINUX_FILENAME
assert config.windows_command == WINDOWS_COMMAND
assert config.windows_filename == WINDOWS_FILENAME
def test_scan_target_configuration():
schema = ScanTargetConfigurationSchema()
config = schema.load(SCAN_TARGET_CONFIGURATION)
assert config.blocked_ips == BLOCKED_IPS
assert config.inaccessible_subnets == INACCESSIBLE_SUBNETS
assert config.local_network_scan == LOCAL_NETWORK_SCAN
assert config.subnets == SUBNETS
def test_icmp_scan_configuration_schema():
schema = ICMPScanConfigurationSchema()
config = schema.load(ICMP_CONFIGURATION)
assert config.timeout == TIMEOUT
def test_tcp_scan_configuration_schema():
schema = TCPScanConfigurationSchema()
config = schema.load(TCP_SCAN_CONFIGURATION)
assert config.timeout == TIMEOUT
assert config.ports == PORTS
def test_network_scan_configuration():
schema = NetworkScanConfigurationSchema()
config = schema.load(NETWORK_SCAN_CONFIGURATION)
assert config.tcp.ports == TCP_SCAN_CONFIGURATION["ports"]
assert config.tcp.timeout == TCP_SCAN_CONFIGURATION["timeout"]
assert config.icmp.timeout == ICMP_CONFIGURATION["timeout"]
assert config.fingerprinters[0].name == FINGERPRINTERS[0]["name"]
assert config.fingerprinters[0].options == FINGERPRINTERS[0]["options"]
assert config.targets.blocked_ips == BLOCKED_IPS
assert config.targets.inaccessible_subnets == INACCESSIBLE_SUBNETS
assert config.targets.local_network_scan == LOCAL_NETWORK_SCAN
assert config.targets.subnets == SUBNETS
def test_exploitation_options_configuration_schema():
ports = [1, 2, 3]
schema = ExploitationOptionsConfigurationSchema()
config = schema.load({"http_ports": ports})
assert config.http_ports == ports
def test_exploiter_configuration_schema():
name = "bond"
options = {"gun": "Walther PPK", "car": "Aston Martin DB5"}
schema = PluginConfigurationSchema()
config = schema.load({"name": name, "options": options})
assert config.name == name
assert config.options == options
def test_exploitation_configuration():
schema = ExploitationConfigurationSchema()
config = schema.load(EXPLOITATION_CONFIGURATION)
config_dict = schema.dump(config)
assert isinstance(config, ExploitationConfiguration)
assert config_dict == EXPLOITATION_CONFIGURATION
def test_propagation_configuration():
schema = PropagationConfigurationSchema()
config = schema.load(PROPAGATION_CONFIGURATION)
config_dict = schema.dump(config)
assert isinstance(config, PropagationConfiguration)
assert isinstance(config.network_scan, NetworkScanConfiguration)
assert isinstance(config.exploitation, ExploitationConfiguration)
assert config.maximum_depth == 5
assert config_dict == PROPAGATION_CONFIGURATION
def test_agent_configuration():
config = AgentConfiguration.from_mapping(AGENT_CONFIGURATION)
config_json = AgentConfiguration.to_json(config)
assert isinstance(config, AgentConfiguration)
assert config.keep_tunnel_open_time == 30
assert isinstance(config.custom_pbas, CustomPBAConfiguration)
assert isinstance(config.post_breach_actions[0], PluginConfiguration)
assert isinstance(config.credential_collectors[0], PluginConfiguration)
assert isinstance(config.payloads[0], PluginConfiguration)
assert isinstance(config.propagation, PropagationConfiguration)
assert json.loads(config_json) == AGENT_CONFIGURATION
def test_incorrect_type():
valid_config = AgentConfiguration.from_mapping(AGENT_CONFIGURATION)
with pytest.raises(InvalidConfigurationError):
valid_config_dict = valid_config.__dict__
valid_config_dict["keep_tunnel_open_time"] = "not_a_float"
AgentConfiguration(**valid_config_dict)
def test_from_dict():
schema = AgentConfigurationSchema()
dict_ = deepcopy(AGENT_CONFIGURATION)
config = AgentConfiguration.from_mapping(dict_)
assert schema.dump(config) == dict_
def test_from_dict__invalid_data():
dict_ = deepcopy(AGENT_CONFIGURATION)
dict_["payloads"] = "payloads"
with pytest.raises(InvalidConfigurationError):
AgentConfiguration.from_mapping(dict_)
def test_from_json():
schema = AgentConfigurationSchema()
dict_ = deepcopy(AGENT_CONFIGURATION)
config = AgentConfiguration.from_json(json.dumps(dict_))
assert isinstance(config, AgentConfiguration)
assert schema.dump(config) == dict_
def test_from_json__invalid_data():
invalid_dict = deepcopy(AGENT_CONFIGURATION)
invalid_dict["payloads"] = "payloads"
with pytest.raises(InvalidConfigurationError):
AgentConfiguration.from_json(json.dumps(invalid_dict))
def test_to_json():
config = deepcopy(AGENT_CONFIGURATION)
assert json.loads(AgentConfiguration.to_json(config)) == AGENT_CONFIGURATION

View File

@ -9,6 +9,8 @@ from _pytest.monkeypatch import MonkeyPatch
MONKEY_BASE_PATH = str(Path(__file__).parent.parent.parent)
sys.path.insert(0, MONKEY_BASE_PATH)
from common.configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfiguration # noqa: E402
@pytest.fixture(scope="session")
def data_for_tests_dir(pytestconfig):
@ -54,3 +56,8 @@ def load_monkey_config(data_for_tests_dir) -> Callable[[str], Dict]:
return json.loads(open(config_path, "r").read())
return inner
@pytest.fixture
def default_agent_configuration() -> AgentConfiguration:
return DEFAULT_AGENT_CONFIGURATION

View File

@ -15,8 +15,3 @@ class TelemetryMessengerSpy(ITelemetryMessenger):
@pytest.fixture
def telemetry_messenger_spy():
return TelemetryMessengerSpy()
@pytest.fixture
def automated_master_config(load_monkey_config):
return load_monkey_config("automated_master_config.json")

View File

@ -2,6 +2,7 @@ from unittest.mock import Mock
import pytest
from common import OperatingSystems
from infection_monkey.exploit.tools.helpers import (
AGENT_BINARY_PATH_LINUX,
AGENT_BINARY_PATH_WIN64,
@ -13,22 +14,28 @@ from infection_monkey.exploit.tools.helpers import (
def _get_host(os):
host = Mock()
host.os = {"type": os}
host.is_windows = lambda: os == OperatingSystems.WINDOWS
return host
@pytest.mark.parametrize(
"os, path", [("linux", AGENT_BINARY_PATH_LINUX), ("windows", AGENT_BINARY_PATH_WIN64)]
"os, path",
[
(OperatingSystems.LINUX, AGENT_BINARY_PATH_LINUX),
(OperatingSystems.WINDOWS, AGENT_BINARY_PATH_WIN64),
],
)
def test_get_agent_dst_path(os, path):
host = _get_host(os)
rand_path = get_agent_dst_path(host)
print(f"{os}: {rand_path}")
# Assert that filename got longer by RAND_SUFFIX_LEN and one dash
assert len(str(rand_path)) == (len(str(path)) + RAND_SUFFIX_LEN + 1)
def test_get_agent_dst_path_randomness():
host = _get_host("windows")
host = _get_host(OperatingSystems.WINDOWS)
path1 = get_agent_dst_path(host)
path2 = get_agent_dst_path(host)
@ -37,7 +44,7 @@ def test_get_agent_dst_path_randomness():
def test_get_agent_dst_path_str_place():
host = _get_host("windows")
host = _get_host(OperatingSystems.WINDOWS)
rand_path = get_agent_dst_path(host)

View File

@ -2,6 +2,7 @@ import logging
import threading
from typing import Dict, Iterable, List, Sequence
from common import OperatingSystems
from infection_monkey.credential_collectors import LMHash, Password, SSHKeypair, Username
from infection_monkey.i_puppet import (
Credentials,
@ -182,19 +183,23 @@ class MockPuppet(IPuppet):
"vulnerable_ports": [22],
"executed_cmds": [],
}
os_windows = "windows"
os_linux = "linux"
successful_exploiters = {
DOT_1: {
"ZerologonExploiter": ExploiterResultData(
False, False, False, os_windows, {}, [], "Zerologon failed"
False, False, False, OperatingSystems.WINDOWS, {}, [], "Zerologon failed"
),
"SSHExploiter": ExploiterResultData(
False, False, False, os_linux, info_ssh, attempts, "Failed exploiting"
False,
False,
False,
OperatingSystems.LINUX,
info_ssh,
attempts,
"Failed exploiting",
),
"WmiExploiter": ExploiterResultData(
True, True, False, os_windows, info_wmi, attempts, None
True, True, False, OperatingSystems.WINDOWS, info_wmi, attempts, None
),
},
DOT_3: {
@ -202,16 +207,22 @@ class MockPuppet(IPuppet):
False,
False,
False,
os_windows,
OperatingSystems.WINDOWS,
info_wmi,
attempts,
"PowerShell Exploiter Failed",
),
"SSHExploiter": ExploiterResultData(
False, False, False, os_linux, info_ssh, attempts, "Failed exploiting"
False,
False,
False,
OperatingSystems.LINUX,
info_ssh,
attempts,
"Failed exploiting",
),
"ZerologonExploiter": ExploiterResultData(
True, False, False, os_windows, {}, [], None
True, False, False, OperatingSystems.WINDOWS, {}, [], None
),
},
}
@ -220,7 +231,13 @@ class MockPuppet(IPuppet):
return successful_exploiters[host.ip_addr][name]
except KeyError:
return ExploiterResultData(
False, False, False, os_linux, {}, [], f"{name} failed for host {host}"
False,
False,
False,
OperatingSystems.LINUX,
{},
[],
f"{name} failed for host {host}",
)
def run_payload(self, name: str, options: Dict, interrupt: threading.Event):

View File

@ -41,14 +41,14 @@ def test_stop_if_cant_get_config_from_island(monkeypatch):
@pytest.fixture
def sleep_and_return_config(automated_master_config):
def sleep_and_return_config(default_agent_configuration):
# Ensure that should_agent_stop times out before get_config() returns to prevent the
# Propagator's sub-threads from hanging
get_config_sleep_time = INTERVAL * (CHECK_FOR_STOP_AGENT_COUNT + 1)
def _inner():
time.sleep(get_config_sleep_time)
return automated_master_config
return default_agent_configuration
return _inner

View File

@ -7,6 +7,11 @@ from unittest.mock import MagicMock
import pytest
from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet
from common import OperatingSystems
from common.configuration.agent_sub_configurations import (
ExploitationConfiguration,
PluginConfiguration,
)
from infection_monkey.master import Exploiter
from infection_monkey.model import VictimHost
@ -34,18 +39,18 @@ def callback():
@pytest.fixture
def exploiter_config():
return {
"options": {"dropper_path_linux": "/tmp/monkey"},
"brute_force": [
{"name": "HadoopExploiter", "supported_os": ["windows"], "options": {"timeout": 10}},
{"name": "SSHExploiter", "supported_os": ["linux"], "options": {}},
{"name": "WmiExploiter", "supported_os": ["windows"], "options": {"timeout": 10}},
],
"vulnerability": [
{"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}},
],
}
def exploiter_config(default_agent_configuration):
brute_force = [
PluginConfiguration(name="MSSQLExploiter", options={"timeout": 10}),
PluginConfiguration(name="SSHExploiter", options={}),
PluginConfiguration(name="WmiExploiter", options={"timeout": 10}),
]
vulnerability = [PluginConfiguration(name="ZerologonExploiter", options={})]
return ExploitationConfiguration(
options=default_agent_configuration.propagation.exploitation.options,
brute_force=brute_force,
vulnerability=vulnerability,
)
@pytest.fixture
@ -105,11 +110,11 @@ def test_exploiter(callback, hosts, hosts_to_exploit, run_exploiters):
host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list)
assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos
assert ("HadoopExploiter", hosts[0]) in host_exploit_combos
assert ("MSSQLExploiter", hosts[0]) in host_exploit_combos
assert ("SSHExploiter", hosts[0]) in host_exploit_combos
assert ("WmiExploiter", hosts[0]) in host_exploit_combos
assert ("ZerologonExploiter", hosts[1]) in host_exploit_combos
assert ("HadoopExploiter", hosts[1]) in host_exploit_combos
assert ("MSSQLExploiter", hosts[1]) in host_exploit_combos
assert ("WmiExploiter", hosts[1]) in host_exploit_combos
assert ("SSHExploiter", hosts[1]) in host_exploit_combos
@ -160,7 +165,7 @@ def test_exploiter_raises_exception(callback, hosts, hosts_to_exploit, run_explo
def test_windows_exploiters_run_on_windows_host(callback, hosts, hosts_to_exploit, run_exploiters):
host = VictimHost("10.0.0.1")
host.os["type"] = "windows"
host.os["type"] = OperatingSystems.WINDOWS
q = enqueue_hosts([host])
run_exploiters(MockPuppet(), 1, q)
@ -172,7 +177,7 @@ def test_windows_exploiters_run_on_windows_host(callback, hosts, hosts_to_exploi
def test_linux_exploiters_run_on_linux_host(callback, hosts, hosts_to_exploit, run_exploiters):
host = VictimHost("10.0.0.1")
host.os["type"] = "linux"
host.os["type"] = OperatingSystems.LINUX
q = enqueue_hosts([host])
run_exploiters(MockPuppet(), 1, q)
@ -196,6 +201,6 @@ def test_all_exploiters_run_on_unknown_host(callback, hosts, hosts_to_exploit, r
host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list)
assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos
assert ("HadoopExploiter", hosts[0]) in host_exploit_combos
assert ("MSSQLExploiter", hosts[0]) in host_exploit_combos
assert ("SSHExploiter", host) in host_exploit_combos
assert ("WmiExploiter", hosts[0]) in host_exploit_combos

View File

@ -5,6 +5,12 @@ from unittest.mock import MagicMock
import pytest
from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet
from common.configuration.agent_sub_configurations import (
ICMPScanConfiguration,
NetworkScanConfiguration,
PluginConfiguration,
TCPScanConfiguration,
)
from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus
from infection_monkey.master import IPScanner
from infection_monkey.network import NetworkAddress
@ -14,11 +20,10 @@ LINUX_OS = "linux"
@pytest.fixture
def scan_config():
return {
"tcp": {
"timeout_ms": 3000,
"ports": [
def scan_config(default_agent_configuration):
tcp_config = TCPScanConfiguration(
timeout=3,
ports=[
22,
445,
3389,
@ -26,16 +31,20 @@ def scan_config():
8008,
3306,
],
},
"icmp": {
"timeout_ms": 1000,
},
"fingerprinters": [
{"name": "HTTPFinger", "options": {}},
{"name": "SMBFinger", "options": {}},
{"name": "SSHFinger", "options": {}},
],
}
)
icmp_config = ICMPScanConfiguration(timeout=1)
fingerprinter_config = [
PluginConfiguration(name="HTTPFinger", options={}),
PluginConfiguration(name="SMBFinger", options={}),
PluginConfiguration(name="SSHFinger", options={}),
]
scan_config = NetworkScanConfiguration(
tcp_config,
icmp_config,
fingerprinter_config,
default_agent_configuration.propagation.network_scan.targets,
)
return scan_config
@pytest.fixture

View File

@ -3,6 +3,11 @@ from unittest.mock import MagicMock
import pytest
from common.configuration.agent_sub_configurations import (
NetworkScanConfiguration,
PropagationConfiguration,
ScanTargetConfiguration,
)
from infection_monkey.i_puppet import (
ExploiterResultData,
FingerprintData,
@ -135,24 +140,37 @@ class StubExploiter:
pass
def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory):
def get_propagation_config(
default_agent_configuration, scan_target_config: ScanTargetConfiguration
):
network_scan = NetworkScanConfiguration(
default_agent_configuration.propagation.network_scan.tcp,
default_agent_configuration.propagation.network_scan.icmp,
default_agent_configuration.propagation.network_scan.fingerprinters,
scan_target_config,
)
propagation_config = PropagationConfiguration(
default_agent_configuration.propagation.maximum_depth,
network_scan,
default_agent_configuration.propagation.exploitation,
)
return propagation_config
def test_scan_result_processing(
telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory, default_agent_configuration
):
p = Propagator(
telemetry_messenger_spy, mock_ip_scanner, StubExploiter(), mock_victim_host_factory, []
)
p.propagate(
{
"targets": {
"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"],
"local_network_scan": False,
"inaccessible_subnets": [],
"blocked_ips": [],
},
"network_scan": {}, # This is empty since MockIPscanner ignores it
"exploiters": {}, # This is empty since StubExploiter ignores it
},
1,
Event(),
targets = ScanTargetConfiguration(
blocked_ips=[],
inaccessible_subnets=[],
local_network_scan=False,
subnets=["10.0.0.1", "10.0.0.2", "10.0.0.3"],
)
propagation_config = get_propagation_config(default_agent_configuration, targets)
p.propagate(propagation_config, 1, Event())
assert len(telemetry_messenger_spy.telemetries) == 3
@ -237,25 +255,20 @@ class MockExploiter:
def test_exploiter_result_processing(
telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory
telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory, default_agent_configuration
):
p = Propagator(
telemetry_messenger_spy, mock_ip_scanner, MockExploiter(), mock_victim_host_factory, []
)
p.propagate(
{
"targets": {
"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"],
"local_network_scan": False,
"inaccessible_subnets": [],
"blocked_ips": [],
},
"network_scan": {}, # This is empty since MockIPscanner ignores it
"exploiters": {}, # This is empty since MockExploiter ignores it
},
1,
Event(),
targets = ScanTargetConfiguration(
blocked_ips=[],
inaccessible_subnets=[],
local_network_scan=False,
subnets=["10.0.0.1", "10.0.0.2", "10.0.0.3"],
)
propagation_config = get_propagation_config(default_agent_configuration, targets)
p.propagate(propagation_config, 1, Event())
exploit_telems = [t for t in telemetry_messenger_spy.telemetries if isinstance(t, ExploitTelem)]
assert len(exploit_telems) == 4
@ -278,7 +291,9 @@ def test_exploiter_result_processing(
assert data["propagation_result"]
def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory):
def test_scan_target_generation(
telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory, default_agent_configuration
):
local_network_interfaces = [NetworkInterface("10.0.0.9", "/29")]
p = Propagator(
telemetry_messenger_spy,
@ -287,20 +302,15 @@ def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_v
mock_victim_host_factory,
local_network_interfaces,
)
p.propagate(
{
"targets": {
"subnet_scan_list": ["10.0.0.0/29", "172.10.20.30"],
"local_network_scan": True,
"blocked_ips": ["10.0.0.3"],
"inaccessible_subnets": ["10.0.0.128/30", "10.0.0.8/29"],
},
"network_scan": {}, # This is empty since MockIPscanner ignores it
"exploiters": {}, # This is empty since MockExploiter ignores it
},
1,
Event(),
targets = ScanTargetConfiguration(
blocked_ips=["10.0.0.3"],
inaccessible_subnets=["10.0.0.128/30", "10.0.0.8/29"],
local_network_scan=True,
subnets=["10.0.0.0/29", "172.10.20.30"],
)
propagation_config = get_propagation_config(default_agent_configuration, targets)
p.propagate(propagation_config, 1, Event())
expected_ip_scan_list = [
"10.0.0.0",
"10.0.0.1",

View File

@ -4,6 +4,7 @@ from unittest.mock import MagicMock
import pytest
from common import OperatingSystems
from infection_monkey.network_scanning import ping
from infection_monkey.network_scanning.ping_scanner import EMPTY_PING_SCAN
@ -91,7 +92,7 @@ def test_linux_ping_success(patch_subprocess_running_ping_with_ping_output):
result = ping("192.168.1.1", 1.0)
assert result.response_received
assert result.os == "linux"
assert result.os == OperatingSystems.LINUX
@pytest.mark.usefixtures("set_os_linux")
@ -109,7 +110,7 @@ def test_windows_ping_success(patch_subprocess_running_ping_with_ping_output):
result = ping("192.168.1.1", 1.0)
assert result.response_received
assert result.os == "windows"
assert result.os == OperatingSystems.WINDOWS
@pytest.mark.usefixtures("set_os_windows")

View File

@ -1,5 +1,6 @@
import pytest
from common import OperatingSystems
from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus
from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter
@ -56,7 +57,7 @@ def test_ssh_os(ssh_fingerprinter):
results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None)
assert results == FingerprintData(
"linux",
OperatingSystems.LINUX,
"Ubuntu-4ubuntu0.2",
{
"tcp-22": {
@ -78,7 +79,7 @@ def test_multiple_os(ssh_fingerprinter):
results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None)
assert results == FingerprintData(
"linux",
OperatingSystems.LINUX,
"Debian",
{
"tcp-22": {

View File

@ -1,32 +1,22 @@
from infection_monkey.utils.propagation import should_propagate
from infection_monkey.utils.propagation import maximum_depth_reached
def get_config(max_depth):
return {"config": {"depth": max_depth}}
def test_should_propagate_current_less_than_max():
max_depth = 2
def test_maximum_depth_reached__current_less_than_max():
maximum_depth = 2
current_depth = 1
config = get_config(max_depth)
assert should_propagate(config, current_depth) is True
assert maximum_depth_reached(maximum_depth, current_depth) is True
def test_should_propagate_current_greater_than_max():
max_depth = 2
def test_maximum_depth_reached__current_greater_than_max():
maximum_depth = 2
current_depth = 3
config = get_config(max_depth)
assert should_propagate(config, current_depth) is False
assert maximum_depth_reached(maximum_depth, current_depth) is False
def test_should_propagate_current_equal_to_max():
max_depth = 2
current_depth = max_depth
def test_maximum_depth_reached__current_equal_to_max():
maximum_depth = 2
current_depth = maximum_depth
config = get_config(max_depth)
assert should_propagate(config, current_depth) is False
assert maximum_depth_reached(maximum_depth, current_depth) is False

View File

@ -0,0 +1,31 @@
from unittest.mock import MagicMock
import pytest
from monkey_island.cc.repository import AgentBinaryRepository, IFileRepository, RetrievalError
LINUX_AGENT_BINARY = b"linux_binary"
WINDOWS_AGENT_BINARY = b"windows_binary"
@pytest.fixture
def error_mock_file_repository():
mock_file_repository = MagicMock(wraps=IFileRepository)
mock_file_repository.open_file = MagicMock(side_effect=OSError)
return mock_file_repository
@pytest.fixture
def agent_binary_repository(error_mock_file_repository):
return AgentBinaryRepository(error_mock_file_repository)
def test_get_linux_binary_retrieval_error(agent_binary_repository):
with pytest.raises(RetrievalError):
agent_binary_repository.get_linux_binary()
def test_get_windows_binary_retrieval_error(agent_binary_repository):
with pytest.raises(RetrievalError):
agent_binary_repository.get_windows_binary()

View File

@ -0,0 +1,35 @@
import pytest
from tests.common.example_agent_configuration import AGENT_CONFIGURATION
from tests.monkey_island import OpenErrorFileRepository, SingleFileRepository
from common.configuration import AgentConfiguration
from monkey_island.cc.repository import FileAgentConfigurationRepository, RetrievalError
@pytest.fixture
def repository(default_agent_configuration):
return FileAgentConfigurationRepository(default_agent_configuration, SingleFileRepository())
def test_store_agent_config(repository):
agent_configuration = AgentConfiguration.from_mapping(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, default_agent_configuration):
retrieved_agent_configuration = repository.get_configuration()
assert retrieved_agent_configuration == default_agent_configuration
def test_get_agent_config_retrieval_error(default_agent_configuration):
repository = FileAgentConfigurationRepository(
default_agent_configuration, OpenErrorFileRepository()
)
with pytest.raises(RetrievalError):
repository.get_configuration()

View File

@ -1,10 +1,12 @@
import io
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from tests.monkey_island.utils import assert_linux_permissions, assert_windows_permissions
from monkey_island.cc.repository import FileRetrievalError, LocalStorageFileRepository
from monkey_island.cc import repository
from monkey_island.cc.repository import LocalStorageFileRepository
from monkey_island.cc.server_utils.file_utils import is_windows_os
@ -131,5 +133,13 @@ def test_remove_nonexistant_file(tmp_path):
def test_open_nonexistant_file(tmp_path):
fss = LocalStorageFileRepository(tmp_path)
with pytest.raises(FileRetrievalError):
with pytest.raises(repository.FileNotFoundError):
fss.open_file("nonexistant_file.txt")
def test_open_locked_file(tmp_path, monkeypatch):
fss = LocalStorageFileRepository(tmp_path)
with patch("builtins.open", Mock(side_effect=Exception())):
with pytest.raises(repository.RetrievalError):
fss.open_file("locked_file.txt")

View File

@ -2,11 +2,14 @@ from unittest.mock import MagicMock
import flask_jwt_extended
import pytest
from tests.common import StubDIContainer
from tests.monkey_island import OpenErrorFileRepository
from tests.unit_tests.monkey_island.conftest import init_mock_app
import monkey_island.cc.app
import monkey_island.cc.resources.auth.auth
import monkey_island.cc.resources.island_mode
from monkey_island.cc.repository import IFileRepository
@pytest.fixture
@ -38,3 +41,12 @@ def get_mock_app(container):
flask_jwt_extended.JWTManager(app)
return app
@pytest.fixture
def open_error_flask_client(build_flask_client):
container = StubDIContainer()
container.register(IFileRepository, OpenErrorFileRepository)
with build_flask_client(container) as flask_client:
yield flask_client

View File

@ -0,0 +1,50 @@
import json
import pytest
from tests.common import StubDIContainer
from tests.common.example_agent_configuration import AGENT_CONFIGURATION
from tests.monkey_island import InMemoryAgentConfigurationRepository
from tests.unit_tests.monkey_island.conftest import get_url_for_resource
from monkey_island.cc.repository import IAgentConfigurationRepository
from monkey_island.cc.resources.agent_configuration import AgentConfiguration
@pytest.fixture
def flask_client(build_flask_client):
container = StubDIContainer()
container.register(IAgentConfigurationRepository, InMemoryAgentConfigurationRepository)
with build_flask_client(container) as flask_client:
yield flask_client
def test_agent_configuration_endpoint(flask_client):
agent_configuration_url = get_url_for_resource(AgentConfiguration)
flask_client.post(
agent_configuration_url, data=json.dumps(AGENT_CONFIGURATION), follow_redirects=True
)
resp = flask_client.get(agent_configuration_url)
assert resp.status_code == 200
assert json.loads(resp.data) == AGENT_CONFIGURATION
def test_agent_configuration_invalid_config(flask_client):
agent_configuration_url = get_url_for_resource(AgentConfiguration)
resp = flask_client.post(
agent_configuration_url, data=json.dumps({"invalid_config": "invalid_stuff"})
)
assert resp.status_code == 400
def test_agent_configuration_invalid_json(flask_client):
agent_configuration_url = get_url_for_resource(AgentConfiguration)
resp = flask_client.post(agent_configuration_url, data="InvalidJson!")
assert resp.status_code == 400

View File

@ -1,36 +1,11 @@
import io
from typing import BinaryIO
import pytest
from tests.common import StubDIContainer
from tests.monkey_island import FILE_CONTENTS, FILE_NAME, MockFileRepository
from tests.unit_tests.monkey_island.conftest import get_url_for_resource
from monkey_island.cc.repository import FileRetrievalError, IFileRepository
from monkey_island.cc.repository import IFileRepository
from monkey_island.cc.resources.pba_file_download import PBAFileDownload
FILE_NAME = "test_file"
FILE_CONTENTS = b"HelloWorld!"
class MockFileRepository(IFileRepository):
def __init__(self):
self._file = io.BytesIO(FILE_CONTENTS)
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
pass
def open_file(self, unsafe_file_name: str) -> BinaryIO:
if unsafe_file_name != FILE_NAME:
raise FileRetrievalError()
return self._file
def delete_file(self, unsafe_file_name: str):
pass
def delete_all_files(self):
pass
@pytest.fixture
def flask_client(build_flask_client):
@ -56,3 +31,11 @@ def test_file_download_endpoint_404(tmp_path, flask_client):
resp = flask_client.get(download_url)
assert resp.status_code == 404
def test_file_download_endpoint_500(tmp_path, open_error_flask_client):
download_url = get_url_for_resource(PBAFileDownload, filename="test")
resp = open_error_flask_client.get(download_url)
assert resp.status_code == 500

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
@ -119,6 +98,15 @@ def test_pba_file_upload_get__file_not_found(flask_client, pba_os, mock_get_conf
assert resp.status_code == 404
@pytest.mark.parametrize("pba_os", [LINUX_PBA_TYPE, WINDOWS_PBA_TYPE])
def test_file_download_endpoint_500(open_error_flask_client, pba_os):
url = get_url_for_resource(FileUpload, target_os=pba_os, filename="bobug_mogus.py")
resp = open_error_flask_client.get(url)
assert resp.status_code == 500
@pytest.mark.parametrize("pba_os", [LINUX_PBA_TYPE, WINDOWS_PBA_TYPE])
def test_pba_file_upload_endpoint(
flask_client, pba_os, mock_get_config_value, mock_set_config_value

View File

@ -25,7 +25,6 @@ def test_format_config_for_agent__credentials_removed():
def test_format_config_for_agent__ransomware_payload():
expected_ransomware_options = {
"ransomware": {
"encryption": {
"enabled": True,
"directories": {
@ -35,24 +34,24 @@ def test_format_config_for_agent__ransomware_payload():
},
"other_behaviors": {"readme": True},
}
}
flat_monkey_config = ConfigService.format_flat_config_for_agent()
assert "payloads" in flat_monkey_config
assert flat_monkey_config["payloads"] == expected_ransomware_options
assert flat_monkey_config["payloads"][0]["name"] == "ransomware"
assert flat_monkey_config["payloads"][0]["options"] == expected_ransomware_options
assert "ransomware" not in flat_monkey_config
def test_format_config_for_agent__pbas():
expected_pbas_config = {
"CommunicateAsBackdoorUser": {},
"ModifyShellStartupFiles": {},
"ScheduleJobs": {},
"Timestomping": {},
"AccountDiscovery": {},
}
expected_pbas_config = [
{"name": "CommunicateAsBackdoorUser", "options": {}},
{"name": "ModifyShellStartupFiles", "options": {}},
{"name": "ScheduleJobs", "options": {}},
{"name": "Timestomping", "options": {}},
{"name": "AccountDiscovery", "options": {}},
]
flat_monkey_config = ConfigService.format_flat_config_for_agent()
assert "post_breach_actions" in flat_monkey_config
@ -93,32 +92,14 @@ def test_format_config_for_agent__propagation():
flat_monkey_config = ConfigService.format_flat_config_for_agent()
assert "propagation" in flat_monkey_config
assert "targets" in flat_monkey_config["propagation"]
assert "network_scan" in flat_monkey_config["propagation"]
assert "exploiters" in flat_monkey_config["propagation"]
def test_format_config_for_agent__propagation_targets():
expected_targets = {
"blocked_ips": ["192.168.1.1", "192.168.1.100"],
"inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"],
"local_network_scan": True,
"subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"],
}
flat_monkey_config = ConfigService.format_flat_config_for_agent()
assert flat_monkey_config["propagation"]["targets"] == expected_targets
assert "blocked_ips" not in flat_monkey_config
assert "inaccessible_subnets" not in flat_monkey_config
assert "local_network_scan" not in flat_monkey_config
assert "subnet_scan_list" not in flat_monkey_config
assert "exploitation" in flat_monkey_config["propagation"]
def test_format_config_for_agent__network_scan():
expected_network_scan_config = {
"tcp": {
"timeout_ms": 3000,
"timeout": 3.0,
"ports": [
22,
80,
@ -136,7 +117,13 @@ def test_format_config_for_agent__network_scan():
],
},
"icmp": {
"timeout_ms": 1000,
"timeout": 1.0,
},
"targets": {
"blocked_ips": ["192.168.1.1", "192.168.1.100"],
"inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"],
"local_network_scan": True,
"subnets": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"],
},
"fingerprinters": [
{"name": "elastic", "options": {}},
@ -161,36 +148,63 @@ def test_format_config_for_agent__network_scan():
assert "finger_classes" not in flat_monkey_config
def test_format_config_for_agent__propagation_network_scan_targets():
expected_targets = {
"blocked_ips": ["192.168.1.1", "192.168.1.100"],
"inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"],
"local_network_scan": True,
"subnets": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"],
}
flat_monkey_config = ConfigService.format_flat_config_for_agent()
assert flat_monkey_config["propagation"]["network_scan"]["targets"] == expected_targets
assert "blocked_ips" not in flat_monkey_config
assert "inaccessible_subnets" not in flat_monkey_config
assert "local_network_scan" not in flat_monkey_config
assert "subnet_scan_list" not in flat_monkey_config
def test_format_config_for_agent__exploiters():
expected_exploiters_config = {
"options": {
"http_ports": [80, 443, 7001, 8008, 8080, 9200],
},
"brute_force": [
{"name": "MSSQLExploiter", "supported_os": ["windows"], "options": {}},
{"name": "PowerShellExploiter", "supported_os": ["windows"], "options": {}},
{"name": "SSHExploiter", "supported_os": ["linux"], "options": {}},
{"name": "MSSQLExploiter", "options": {}},
{
"name": "PowerShellExploiter",
"options": {},
},
{"name": "SSHExploiter", "options": {}},
{
"name": "SmbExploiter",
"supported_os": ["windows"],
"options": {"smb_download_timeout": 30},
},
{
"name": "WmiExploiter",
"supported_os": ["windows"],
"options": {"smb_download_timeout": 30},
},
],
"vulnerability": [
{"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}},
{"name": "Log4ShellExploiter", "supported_os": ["linux", "windows"], "options": {}},
{"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}},
{
"name": "HadoopExploiter",
"options": {},
},
{
"name": "Log4ShellExploiter",
"options": {},
},
{
"name": "ZerologonExploiter",
"options": {},
},
],
}
flat_monkey_config = ConfigService.format_flat_config_for_agent()
assert "propagation" in flat_monkey_config
assert "exploiters" in flat_monkey_config["propagation"]
assert "exploitation" in flat_monkey_config["propagation"]
assert flat_monkey_config["propagation"]["exploiters"] == expected_exploiters_config
assert flat_monkey_config["propagation"]["exploitation"] == expected_exploiters_config
assert "exploiter_classes" not in flat_monkey_config

View File

@ -1,4 +1,5 @@
from datetime import datetime
from enum import Enum
from unittest import TestCase
import bson
@ -44,3 +45,11 @@ class TestRepresentations(TestCase):
}
),
)
def test_normalize__enum(self):
class BogusEnum(Enum):
bogus_val = "Bogus"
my_obj = {"something": "something", "my_enum": BogusEnum.bogus_val}
assert {"something": "something", "my_enum": "bogus_val"} == normalize_obj(my_obj)

View File

@ -23,6 +23,7 @@ log_cli_date_format = "%H:%M:%S"
addopts = "-v --capture=sys tests/unit_tests"
norecursedirs = "node_modules dist"
markers = ["slow: mark test as slow"]
pythonpath = "./monkey"
[tool.vulture]
exclude = ["monkey/monkey_island/cc/ui/", "monkey/tests/", "monkey/monkey_island/docs/"]

View File

@ -184,6 +184,19 @@ architecture # unused variable (monkey/infection_monkey/exploit/caching_agent_r
response_code # unused variable (monkey/monkey_island/cc/services/aws/aws_command_runner.py:26)
release_convention # unused method (monkey/common/di_container.py:174)
# Agent Configuration
_make_plugin_configuration # unused method (monkey/common/configuration/agent_configuration.py:19)
_make_custom_pba_configuration # unused method (monkey/common/configuration/agent_configuration.py:24)
_make_exploiter_configuration # unused method (monkey/common/configuration/agent_configuration.py:69)
_make_exploitation_options_configuration # unused method (monkey/common/configuration/agent_configuration.py:86)
_make_scan_target_configuration # unused method (monkey/common/configuration/agent_configuration.py:105)
_make_icmp_scan_configuration # unused method (monkey/common/configuration/agent_configuration.py:107)
_make_tcp_scan_configuration # unused method (monkey/common/configuration/agent_configuration.py:122)
_make_network_scan_configuration # unused method (monkey/common/configuration/agent_configuration.py:110)
_make_propagation_configuration # unused method (monkey/common/configuration/agent_configuration.py:167)
_make_agent_configuration # unused method (monkey/common/configuration/agent_configuration.py:192)
# TODO DELETE AFTER RESOURCE REFACTORING
NetworkMap
Arc.dst_machine