From a3a9ca9963c4fc67e46a5eeeb5d067d9c764d2d5 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Thu, 8 Aug 2024 21:20:20 +0100 Subject: [PATCH] #2768 - Fixed issue causing main port to not be included in list of open ports. documented the configuration of listen_on_ports. added test that tests listen_on_ports configuration from yaml. --- CHANGELOG.md | 11 +++++- .../system/common/common_configuration.rst | 32 +++++++++++++++ src/primaite/game/game.py | 22 ++++++++++- .../simulator/system/core/software_manager.py | 29 ++++++++------ ...ic_node_with_software_listening_ports.yaml | 39 +++++++++++++++++++ .../system/test_service_listening_on_ports.py | 20 ++++++++++ 6 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 tests/assets/configs/basic_node_with_software_listening_ports.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d999607..c354aa14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Random Number Generator Seeding by specifying a random number seed in the config file. - Implemented Terminal service class, providing a generic terminal simulation. +- Added `User`, `UserManager` and `UserSessionManager` to enable the creation of user accounts and login on Nodes. +- Added a `listen_on_ports` set in the `IOSoftware` class to enable software listening on ports in addition to the + main port they're assigned. ### Changed -- Removed the install/uninstall methods in the node class and made the software manager install/uninstall handle all of their functionality. +- Updated `SoftwareManager` `install` and `uninstall` to handle all functionality that was being done at the `install` + and `uninstall` methods in the `Node` class. +- Updated the `receive_payload_from_session_manager` method in `SoftwareManager` so that it now sends a copy of the + payload to any software listening on the destination port of the `Frame`. + +### Removed +- Removed the `install` and `uninstall` methods in the `Node` class. ## [3.2.0] - 2024-07-18 diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index e35ee378..420166dd 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -25,3 +25,35 @@ The configuration options are the attributes that fall under the options for an Optional. Default value is ``2``. The number of timesteps the |SOFTWARE_NAME| will remain in a ``FIXING`` state before going into a ``GOOD`` state. + + +``listen_on_ports`` +""""""""""""""""""" + +The set of ports to listen on. This is in addition to the main port the software is designated. This set can either be +the string name of ports or the port integers + +Example: + +.. code-block:: yaml + + simulation: + network: + nodes: + - hostname: client + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + services: + - type: DatabaseService + options: + backup_server_ip: 10.10.1.12 + listen_on_ports: + - 631 + applications: + - type: WebBrowser + options: + target_url: http://sometech.ai + listen_on_ports: + - SMB diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 6a97ad25..3d3caed9 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import numpy as np from pydantic import BaseModel, ConfigDict @@ -44,8 +44,10 @@ from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer +from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.terminal.terminal import Terminal from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.simulator.system.software import Software _LOGGER = getLogger(__name__) @@ -328,6 +330,21 @@ class PrimaiteGame: user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa for user_cfg in node_cfg["users"]: user_manager.add_user(**user_cfg, bypass_can_perform_action=True) + + def _set_software_listen_on_ports(software: Union[Software, Service], software_cfg: Dict): + """Set listener ports on software.""" + listen_on_ports = [] + for port_id in set(software_cfg.get("options", {}).get("listen_on_ports", [])): + print("yes", port_id) + port = None + if isinstance(port_id, int): + port = Port(port_id) + elif isinstance(port_id, str): + port = Port[port_id] + if port: + listen_on_ports.append(port) + software.listen_on_ports = set(listen_on_ports) + if "services" in node_cfg: for service_cfg in node_cfg["services"]: new_service = None @@ -341,6 +358,7 @@ class PrimaiteGame: if "fix_duration" in service_cfg.get("options", {}): new_service.fixing_duration = service_cfg["options"]["fix_duration"] + _set_software_listen_on_ports(new_service, service_cfg) # start the service new_service.start() else: @@ -390,6 +408,8 @@ class PrimaiteGame: _LOGGER.error(msg) raise ValueError(msg) + _set_software_listen_on_ports(new_application, application_cfg) + # run the application new_application.run() diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 7b36097b..d45611ed 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -237,19 +237,24 @@ class SoftwareManager: self.software.get("NMAP").receive(payload=payload, session_id=session_id) return main_receiver = self.port_protocol_mapping.get((port, protocol), None) - listening_receivers = [software for software in self.software.values() if port in software.listen_on_ports] - receivers = [main_receiver] + listening_receivers if main_receiver else listening_receivers - if receivers: - for receiver in receivers: - receiver.receive( - payload=deepcopy(payload), - session_id=session_id, - from_network_interface=from_network_interface, - frame=frame, - ) - else: + if main_receiver: + main_receiver.receive( + payload=payload, session_id=session_id, from_network_interface=from_network_interface, frame=frame + ) + listening_receivers = [ + software + for software in self.software.values() + if port in software.listen_on_ports and software != main_receiver + ] + for receiver in listening_receivers: + receiver.receive( + payload=deepcopy(payload), + session_id=session_id, + from_network_interface=from_network_interface, + frame=frame, + ) + if not main_receiver and not listening_receivers: self.sys_log.warning(f"No service or application found for port {port} and protocol {protocol}") - pass def show(self, markdown: bool = False): """ diff --git a/tests/assets/configs/basic_node_with_software_listening_ports.yaml b/tests/assets/configs/basic_node_with_software_listening_ports.yaml new file mode 100644 index 00000000..53eee87f --- /dev/null +++ b/tests/assets/configs/basic_node_with_software_listening_ports.yaml @@ -0,0 +1,39 @@ +io_settings: + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True + + +game: + max_episode_length: 256 + ports: + - ARP + protocols: + - ICMP + - UDP + + +simulation: + network: + nodes: + - hostname: client + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + services: + - type: DatabaseService + options: + backup_server_ip: 10.10.1.12 + listen_on_ports: + - 631 + applications: + - type: WebBrowser + options: + target_url: http://sometech.ai + listen_on_ports: + - SMB diff --git a/tests/integration_tests/system/test_service_listening_on_ports.py b/tests/integration_tests/system/test_service_listening_on_ports.py index 0cb1ad54..fd502a70 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -1,13 +1,17 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any, Dict, List, Set +import yaml from pydantic import Field +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.service import Service +from tests import TEST_ASSETS_ROOT class _DatabaseListener(Service): @@ -62,3 +66,19 @@ def test_http_listener(client_server): assert db_connection.query("SELECT") assert len(server_db_listener.payloads_received) == 3 + + +def test_set_listen_on_ports_from_config(): + config_path = TEST_ASSETS_ROOT / "configs" / "basic_node_with_software_listening_ports.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + client: Computer = network.get_node_by_hostname("client") + assert Port.SMB in client.software_manager.get_open_ports() + assert Port.IPP in client.software_manager.get_open_ports() + + web_browser = client.software_manager.software["WebBrowser"] + + assert not web_browser.listen_on_ports.difference({Port.SMB, Port.IPP})