Island: Handle network services in TCP scan events

This commit is contained in:
vakarisz 2022-10-07 16:07:04 +03:00
parent c5c8bc1d2f
commit d3c2d95a69
6 changed files with 103 additions and 14 deletions

View File

@ -90,15 +90,15 @@ class ScanEventHandler:
self._machine_repository.upsert_machine(machine) self._machine_repository.upsert_machine(machine)
def _update_network_services(self, target: Machine, event: TCPScanEvent): def _update_network_services(self, target: Machine, event: TCPScanEvent):
for port in self._get_open_ports(event): network_services = {
socket_addr = SocketAddress(ip=event.target, port=port) SocketAddress(ip=event.target, port=port): NetworkService.UNKNOWN
target.network_services[socket_addr] = NetworkService.UNKNOWN for port in self._get_open_ports(event)
}
self._machine_repository.upsert_machine(target) self._machine_repository.upsert_network_services(target.id, network_services)
@staticmethod @staticmethod
def _get_open_ports(event: TCPScanEvent) -> List[int]: def _get_open_ports(event: TCPScanEvent) -> List[int]:
return (port for port, status in event.ports.items() if status == PortStatus.OPEN) return [port for port, status in event.ports.items() if status == PortStatus.OPEN]
def _update_nodes(self, target_machine: Machine, event: ScanEvent): def _update_nodes(self, target_machine: Machine, event: ScanEvent):
src_machine = self._get_source_machine(event) src_machine = self._get_source_machine(event)

View File

@ -3,6 +3,7 @@ from ipaddress import IPv4Interface
from typing import Any, Dict, Mapping, Optional, Sequence from typing import Any, Dict, Mapping, Optional, Sequence
from pydantic import Field, validator from pydantic import Field, validator
from typing_extensions import TypeAlias
from common import OperatingSystem from common import OperatingSystem
from common.base_models import MutableInfectionMonkeyBaseModel, MutableInfectionMonkeyModelConfig from common.base_models import MutableInfectionMonkeyBaseModel, MutableInfectionMonkeyModelConfig
@ -11,6 +12,8 @@ from common.types import HardwareID, NetworkService, SocketAddress
from . import MachineID from . import MachineID
NetworkServices: TypeAlias = Dict[SocketAddress, NetworkService]
def _serialize_network_services(machine_dict: Dict, *, default): def _serialize_network_services(machine_dict: Dict, *, default):
machine_dict["network_services"] = { machine_dict["network_services"] = {
@ -61,7 +64,7 @@ class Machine(MutableInfectionMonkeyBaseModel):
hostname: str = "" hostname: str = ""
"""The hostname of the machine""" """The hostname of the machine"""
network_services: Mapping[SocketAddress, NetworkService] = Field(default_factory=dict) network_services: NetworkServices = Field(default_factory=dict)
"""All network services found running on the machine""" """All network services found running on the machine"""
_make_immutable_sequence = validator("network_interfaces", pre=True, allow_reuse=True)( _make_immutable_sequence = validator("network_interfaces", pre=True, allow_reuse=True)(

View File

@ -4,6 +4,7 @@ from typing import Sequence
from common.types import HardwareID from common.types import HardwareID
from monkey_island.cc.models import Machine, MachineID from monkey_island.cc.models import Machine, MachineID
from monkey_island.cc.models.machine import NetworkServices
class IMachineRepository(ABC): class IMachineRepository(ABC):
@ -29,6 +30,16 @@ class IMachineRepository(ABC):
:raises StorageError: If an error occurs while attempting to store the `Machine` :raises StorageError: If an error occurs while attempting to store the `Machine`
""" """
@abstractmethod
def upsert_network_services(self, machine_id: MachineID, services: NetworkServices):
"""
Add/update network services on the machine
:param machine_id: ID of machine with services to be updated
:param services: Network services to be added to machine model
:raises UnknownRecordError: If the Machine is not found
:raises StorageError: If an error occurs while attempting to add/store the services
"""
@abstractmethod @abstractmethod
def get_machine_by_id(self, machine_id: MachineID) -> Machine: def get_machine_by_id(self, machine_id: MachineID) -> Machine:
""" """

View File

@ -7,8 +7,10 @@ from pymongo import MongoClient
from common.types import HardwareID from common.types import HardwareID
from monkey_island.cc.models import Machine, MachineID from monkey_island.cc.models import Machine, MachineID
from ..models.machine import NetworkServices
from . import IMachineRepository, RemovalError, RetrievalError, StorageError, UnknownRecordError from . import IMachineRepository, RemovalError, RetrievalError, StorageError, UnknownRecordError
from .consts import MONGO_OBJECT_ID_KEY from .consts import MONGO_OBJECT_ID_KEY
from .utils import DOT_REPLACEMENT, mongo_dot_decoder, mongo_dot_encoder
class MongoMachineRepository(IMachineRepository): class MongoMachineRepository(IMachineRepository):
@ -32,8 +34,9 @@ class MongoMachineRepository(IMachineRepository):
def upsert_machine(self, machine: Machine): def upsert_machine(self, machine: Machine):
try: try:
machine_dict = mongo_dot_encoder(machine.dict(simplify=True))
result = self._machines_collection.replace_one( result = self._machines_collection.replace_one(
{"id": machine.id}, machine.dict(simplify=True), upsert=True {"id": machine.id}, machine_dict, upsert=True
) )
except Exception as err: except Exception as err:
raise StorageError(f'Error updating machine with ID "{machine.id}": {err}') raise StorageError(f'Error updating machine with ID "{machine.id}": {err}')
@ -44,8 +47,19 @@ class MongoMachineRepository(IMachineRepository):
f"but no machines were inserted" f"but no machines were inserted"
) )
def upsert_network_services(self, machine_id: MachineID, services: NetworkServices):
machine = self.get_machine_by_id(machine_id)
try:
machine.network_services.update(services)
self.upsert_machine(machine)
except Exception as err:
raise StorageError(f"Failed upserting the machine or adding services") from err
def get_machine_by_id(self, machine_id: MachineID) -> Machine: def get_machine_by_id(self, machine_id: MachineID) -> Machine:
return self._find_one("id", machine_id) machine = self._find_one("id", machine_id)
if not machine:
raise UnknownRecordError(f"Machine with id {machine_id} not found")
return machine
def get_machine_by_hardware_id(self, hardware_id: HardwareID) -> Machine: def get_machine_by_hardware_id(self, hardware_id: HardwareID) -> Machine:
return self._find_one("hardware_id", hardware_id) return self._find_one("hardware_id", hardware_id)
@ -61,6 +75,7 @@ class MongoMachineRepository(IMachineRepository):
if machine_dict is None: if machine_dict is None:
raise UnknownRecordError(f'Unknown machine with "{key} == {search_value}"') raise UnknownRecordError(f'Unknown machine with "{key} == {search_value}"')
machine_dict = mongo_dot_decoder(machine_dict)
return Machine(**machine_dict) return Machine(**machine_dict)
def get_machines(self) -> Sequence[Machine]: def get_machines(self) -> Sequence[Machine]:
@ -69,10 +84,10 @@ class MongoMachineRepository(IMachineRepository):
except Exception as err: except Exception as err:
raise RetrievalError(f"Error retrieving machines: {err}") raise RetrievalError(f"Error retrieving machines: {err}")
return [Machine(**m) for m in cursor] return [Machine(**mongo_dot_decoder(m)) for m in cursor]
def get_machines_by_ip(self, ip: IPv4Address) -> Sequence[Machine]: def get_machines_by_ip(self, ip: IPv4Address) -> Sequence[Machine]:
ip_regex = "^" + str(ip).replace(".", "\\.") + "\\/.*$" ip_regex = "^" + str(ip).replace(".", DOT_REPLACEMENT) + "\\/.*$"
query = {"network_interfaces": {"$elemMatch": {"$regex": ip_regex}}} query = {"network_interfaces": {"$elemMatch": {"$regex": ip_regex}}}
try: try:
@ -80,7 +95,7 @@ class MongoMachineRepository(IMachineRepository):
except Exception as err: except Exception as err:
raise RetrievalError(f'Error retrieving machines with ip "{ip}": {err}') raise RetrievalError(f'Error retrieving machines with ip "{ip}": {err}')
machines = [Machine(**m) for m in cursor] machines = [Machine(**mongo_dot_decoder(m)) for m in cursor]
if len(machines) == 0: if len(machines) == 0:
raise UnknownRecordError(f'No machines found with IP "{ip}"') raise UnknownRecordError(f'No machines found with IP "{ip}"')

View File

@ -8,7 +8,7 @@ import pytest
from common import OperatingSystem from common import OperatingSystem
from common.agent_events import PingScanEvent, TCPScanEvent from common.agent_events import PingScanEvent, TCPScanEvent
from common.types import PortStatus, SocketAddress from common.types import NetworkService, PortStatus, SocketAddress
from monkey_island.cc.agent_event_handlers import ScanEventHandler from monkey_island.cc.agent_event_handlers import ScanEventHandler
from monkey_island.cc.models import Agent, CommunicationType, Machine, Node from monkey_island.cc.models import Agent, CommunicationType, Machine, Node
from monkey_island.cc.repository import ( from monkey_island.cc.repository import (
@ -74,6 +74,11 @@ TCP_SCAN_EVENT = TCPScanEvent(
ports={22: PortStatus.OPEN, 80: PortStatus.OPEN, 8080: PortStatus.CLOSED}, ports={22: PortStatus.OPEN, 80: PortStatus.OPEN, 8080: PortStatus.CLOSED},
) )
EXPECTED_NETWORK_SERVICES = {
SocketAddress(ip=TARGET_MACHINE_IP, port=22): NetworkService.UNKNOWN,
SocketAddress(ip=TARGET_MACHINE_IP, port=80): NetworkService.UNKNOWN,
}
TCP_CONNECTIONS = { TCP_CONNECTIONS = {
TARGET_MACHINE_ID: ( TARGET_MACHINE_ID: (
SocketAddress(ip=TARGET_MACHINE_IP, port=22), SocketAddress(ip=TARGET_MACHINE_IP, port=22),
@ -382,3 +387,11 @@ def test_failed_scan(
assert not node_repository.upsert_communication.called assert not node_repository.upsert_communication.called
assert not machine_repository.upsert_machine.called assert not machine_repository.upsert_machine.called
def test_network_services_handling(scan_event_handler, machine_repository):
scan_event_handler.handle_tcp_scan_event(TCP_SCAN_EVENT)
machine_repository.upsert_network_services.assert_called_with(
TARGET_MACHINE_ID, EXPECTED_NETWORK_SERVICES
)

View File

@ -6,6 +6,7 @@ import mongomock
import pytest import pytest
from common import OperatingSystem from common import OperatingSystem
from common.types import NetworkService, SocketAddress
from monkey_island.cc.models import Machine from monkey_island.cc.models import Machine
from monkey_island.cc.repository import ( from monkey_island.cc.repository import (
IMachineRepository, IMachineRepository,
@ -15,6 +16,7 @@ from monkey_island.cc.repository import (
StorageError, StorageError,
UnknownRecordError, UnknownRecordError,
) )
from monkey_island.cc.repository.utils import mongo_dot_encoder
MACHINES = ( MACHINES = (
Machine( Machine(
@ -32,6 +34,10 @@ MACHINES = (
operating_system=OperatingSystem.WINDOWS, operating_system=OperatingSystem.WINDOWS,
operating_system_version="eXtra Problems", operating_system_version="eXtra Problems",
hostname="hal", hostname="hal",
network_services={
SocketAddress(ip="192.168.1.11", port=80): NetworkService.UNKNOWN,
SocketAddress(ip="192.168.1.12", port=80): NetworkService.UNKNOWN,
},
), ),
Machine( Machine(
id=3, id=3,
@ -40,6 +46,10 @@ MACHINES = (
operating_system=OperatingSystem.WINDOWS, operating_system=OperatingSystem.WINDOWS,
operating_system_version="Vista", operating_system_version="Vista",
hostname="smith", hostname="smith",
network_services={
SocketAddress(ip="192.168.1.11", port=80): NetworkService.UNKNOWN,
SocketAddress(ip="192.168.1.11", port=22): NetworkService.UNKNOWN,
},
), ),
Machine( Machine(
id=4, id=4,
@ -51,11 +61,24 @@ MACHINES = (
), ),
) )
SERVICES_TO_ADD = {
SocketAddress(ip="192.168.1.11", port=80): NetworkService.UNKNOWN,
SocketAddress(ip="192.168.1.11", port=22): NetworkService.UNKNOWN,
}
EXPECTED_SERVICES_1 = EXPECTED_SERVICES_3 = SERVICES_TO_ADD
EXPECTED_SERVICES_2 = {
**SERVICES_TO_ADD,
SocketAddress(ip="192.168.1.12", port=80): NetworkService.UNKNOWN,
}
@pytest.fixture @pytest.fixture
def mongo_client() -> mongomock.MongoClient: def mongo_client() -> mongomock.MongoClient:
client = mongomock.MongoClient() client = mongomock.MongoClient()
client.monkey_island.machines.insert_many((m.dict(simplify=True) for m in MACHINES)) client.monkey_island.machines.insert_many(
(mongo_dot_encoder(m.dict(simplify=True)) for m in MACHINES)
)
return client return client
@ -264,3 +287,27 @@ def test_usable_after_reset(machine_repository):
def test_reset__removal_error(error_raising_machine_repository): def test_reset__removal_error(error_raising_machine_repository):
with pytest.raises(RemovalError): with pytest.raises(RemovalError):
error_raising_machine_repository.reset() error_raising_machine_repository.reset()
@pytest.mark.parametrize(
"machine_id, expected_services",
[
(MACHINES[0].id, EXPECTED_SERVICES_1),
(MACHINES[1].id, EXPECTED_SERVICES_2),
(MACHINES[2].id, EXPECTED_SERVICES_3),
],
)
def test_service_upsert(machine_id, expected_services, machine_repository):
machine_repository.upsert_network_services(machine_id, SERVICES_TO_ADD)
assert machine_repository.get_machine_by_id(machine_id).network_services == expected_services
def test_service_upsert__machine_not_found(machine_repository):
with pytest.raises(UnknownRecordError):
machine_repository.upsert_network_services(machine_id=999, services=SERVICES_TO_ADD)
def test_service_upsert__error_on_storage(machine_repository):
malformed_services = 3
with pytest.raises(StorageError):
machine_repository.upsert_network_services(MACHINES[0].id, malformed_services)